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.
Files changed (106) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.codex-plugin/plugin.json +46 -0
  3. package/LICENSE +674 -0
  4. package/README.md +128 -0
  5. package/README.zh.md +126 -0
  6. package/bin/bookmark-research.js +30 -0
  7. package/config/providers.json +98 -0
  8. package/docs/bookmark-research-architecture.md +124 -0
  9. package/docs/cli-launcher-plan.md +58 -0
  10. package/docs/details.en.md +272 -0
  11. package/docs/details.md +93 -0
  12. package/docs/harness-compatibility.md +304 -0
  13. package/docs/harness-sources.json +342 -0
  14. package/docs/host-validation.json +298 -0
  15. package/docs/install-design-0.2.0.md +57 -0
  16. package/docs/installation.en.md +197 -0
  17. package/docs/installation.md +316 -0
  18. package/docs/instructions.en.md +42 -0
  19. package/docs/instructions.md +42 -0
  20. package/docs/mcp-aggregation-0.2.0.md +92 -0
  21. package/docs/multi-host-installation-plan.md +163 -0
  22. package/docs/prompt-reference.en.md +104 -0
  23. package/docs/prompt-reference.md +104 -0
  24. package/docs/provider-smoke-0.2.0.json +235 -0
  25. package/docs/release-notes-0.5.0-beta.1.md +33 -0
  26. package/docs/research-0.2.0.md +140 -0
  27. package/docs/research-sources-0.2.0.json +9360 -0
  28. package/docs/user-guide.en.md +74 -0
  29. package/docs/user-guide.md +74 -0
  30. package/docs/validation-0.2.0.md +126 -0
  31. package/docs/validation-0.4.0.md +154 -0
  32. package/docs/wiki-quality.md +105 -0
  33. package/docs/wiki-quality.zh.md +105 -0
  34. package/hosts/claude/runtime.js +13 -0
  35. package/hosts/codex/delegate.md +14 -0
  36. package/hosts/codex/prepare.py +61 -0
  37. package/hosts/dsh/plugin.js +36 -0
  38. package/hosts/dsh/runtime.js +14 -0
  39. package/hosts/dsh/workflow-call.py +45 -0
  40. package/hosts/pi/register-workflow.py +72 -0
  41. package/hosts/pi/runtime.js +18 -0
  42. package/hosts/shared/research-call.py +76 -0
  43. package/hosts/shared/research-flow.js +208 -0
  44. package/hosts/shared/workflow.json +15 -0
  45. package/install.sh +310 -0
  46. package/package.json +12 -0
  47. package/scripts/build_zip.py +143 -0
  48. package/scripts/export_bundle.py +496 -0
  49. package/scripts/host_assets.py +70 -0
  50. package/scripts/host_clients.py +196 -0
  51. package/scripts/host_install.py +277 -0
  52. package/scripts/install.py +593 -0
  53. package/scripts/launcher.py +186 -0
  54. package/scripts/verify_fixture.py +220 -0
  55. package/scripts/verify_live.py +62 -0
  56. package/scripts/verify_quality.py +87 -0
  57. package/skills/bookmark-research/SKILL.md +80 -0
  58. package/skills/bookmark-research/agents/openai.yaml +3 -0
  59. package/skills/bookmark-research/references/cli.md +150 -0
  60. package/skills/bookmark-research/references/deep-research.md +164 -0
  61. package/skills/bookmark-research/references/github-and-sync.md +30 -0
  62. package/skills/bookmark-research/references/host-workflows.md +101 -0
  63. package/skills/bookmark-research/references/package-semantics.md +78 -0
  64. package/skills/bookmark-research/references/research-methods.md +51 -0
  65. package/skills/bookmark-research/references/research-services.md +72 -0
  66. package/skills/bookmark-research/references/research-workflow.md +57 -0
  67. package/skills/bookmark-research/references/settings-and-archive.md +104 -0
  68. package/skills/bookmark-research/references/source-lifecycle.md +75 -0
  69. package/skills/bookmark-research/references/wiki-and-evaluation.md +63 -0
  70. package/skills/bookmark-research/references/zh/cli.md +150 -0
  71. package/skills/bookmark-research/references/zh/deep-research.md +164 -0
  72. package/skills/bookmark-research/references/zh/github-and-sync.md +30 -0
  73. package/skills/bookmark-research/references/zh/host-workflows.md +101 -0
  74. package/skills/bookmark-research/references/zh/package-semantics.md +78 -0
  75. package/skills/bookmark-research/references/zh/research-methods.md +51 -0
  76. package/skills/bookmark-research/references/zh/research-services.md +70 -0
  77. package/skills/bookmark-research/references/zh/research-workflow.md +57 -0
  78. package/skills/bookmark-research/references/zh/settings-and-archive.md +104 -0
  79. package/skills/bookmark-research/references/zh/skill-guide.md +75 -0
  80. package/skills/bookmark-research/references/zh/source-lifecycle.md +75 -0
  81. package/skills/bookmark-research/references/zh/wiki-and-evaluation.md +63 -0
  82. package/src/archive.py +302 -0
  83. package/src/bookmark_index.py +913 -0
  84. package/src/cli.py +435 -0
  85. package/src/credentials.py +82 -0
  86. package/src/evaluation.py +337 -0
  87. package/src/jina.py +96 -0
  88. package/src/mcp_server.py +641 -0
  89. package/src/onboarding.py +352 -0
  90. package/src/provider_adapters.py +152 -0
  91. package/src/readiness.py +283 -0
  92. package/src/remote_mcp.py +295 -0
  93. package/src/research.py +1334 -0
  94. package/src/research_coverage.py +160 -0
  95. package/src/research_services.py +684 -0
  96. package/src/routing.py +202 -0
  97. package/src/search_results.py +244 -0
  98. package/src/service_http.py +139 -0
  99. package/src/settings.py +247 -0
  100. package/src/source_inputs.py +201 -0
  101. package/src/source_manager.py +348 -0
  102. package/src/source_watcher.py +68 -0
  103. package/src/textio.py +9 -0
  104. package/src/tui.py +586 -0
  105. package/src/web_search.py +603 -0
  106. package/src/wiki.py +383 -0
@@ -0,0 +1,105 @@
1
+ # 经审阅的 Wiki 与有标注的研究评测
2
+
3
+ [English](wiki-quality.md) · **中文**
4
+
5
+ 宿主负责撰写、审阅知识页;插件保存证据和修订,核对完整性并检索整理后的正文。评测使用明确标注的输出,不调用模型,也不从文字匹配推断语义支持。
6
+
7
+ ## Wiki API
8
+
9
+ `WikiStore(directory=None, settings=None, research_sessions=None)` 优先使用显式目录,其次 `settings.wiki.directory`,默认数据目录中的 `wiki/`。插件目录和画布包目录会被拒绝。研究证据仍保存在原研究档案中,位于输入包之外。
10
+
11
+ ```python
12
+ from wiki import WikiStore
13
+
14
+ wiki = WikiStore(research_sessions=sessions)
15
+ page = {
16
+ "title": "可复用会话",
17
+ "kind": "topic", # 也可为 entity
18
+ "sections": [{
19
+ "heading": "已确认行为",
20
+ "text": "宿主已审阅的综合说明,保留相关限制。",
21
+ "claims": [{"research_id": research_id, "claim_id": "c1"}],
22
+ }],
23
+ "links": [],
24
+ "review": {
25
+ "method": "model", # 也可为 human
26
+ "reviewer": "真实模型及版本或人工审阅者",
27
+ "note": "说明实际检查的范围和证据支持。",
28
+ },
29
+ }
30
+ written = wiki.write("reusable-sessions", page, "整理已审阅结论", expected_revision=0)
31
+ current = wiki.get("reusable-sessions")
32
+ wiki.write("reusable-sessions", revised_page, "解释新增限制", expected_revision=current["revision"])
33
+ ```
34
+
35
+ 每节必须明确引用研究 claim。只有 claim 仍有效、所有来源均为 accepted、引文存在于已存正文且正文哈希一致时才接受。Wiki 快照保留 research ID、claim ID、source ID、原清单 ID、书签实例引用、原样引文、审阅、检索元数据和产物哈希。每页最多 24 个不同 claim,过大主题需拆分。
36
+
37
+ 审阅声明说明正文是否受支持、实体与范围是否正确、是否保留不确定性和反证。程序记录声明及 rubric,不能证明署名的人或模型实际做了判断。写入成功不是独立语义认可。
38
+
39
+ 导入的外部报告始终是二手证据,保留其 provenance 与报告原句;参考文献不会被扩充成声称已读的原始网页或覆盖来源。
40
+
41
+ ### 修订、链接与检查
42
+
43
+ 每次更新在 `revisions/<page_id>/` 写入不可变 JSON 和渲染 Markdown,再原子更新 `index.json`。索引是发布点。发布失败可能留下未引用文件,旧版仍可读取。expected_revision=0 用于创建;更新必须指定调用者实际读过的版本。写入中断后先检查当前 revision,再判断是否重试。
44
+
45
+ 交叉链接形如 `{"page_id":"existing-page","relation":"宿主对关系的解释"}`,目标页必须已存在。渲染链接指向写入时的目标版本,保留历史含义。目标后续变化会产生 lint 提示,宿主可明确修订关系。旧页版本可通过 `get(page_id, revision=N)` 读取。
46
+
47
+ | 方法 | 返回 |
48
+ | --- | --- |
49
+ | `write(page_id, page, change_note, expected_revision=0)` | 修订元数据与绝对产物路径 |
50
+ | `get(page_id, revision=None)` | 正文、证据快照、更新记录与当前 provenance 核验 |
51
+ | `list(offset=0, limit=20)` | 当前页元数据的分页 |
52
+ | `lint(page_id=None)` | 来源审阅、有效 claim、引文、哈希、链接和历史问题 |
53
+ | `search(query, offset=0, limit=10)` | 匹配当前 Wiki 正文,排除已失效的页 |
54
+
55
+ Lint 检测后续来源拒绝、结论撤回、来源/claim 变化和缺失/改动的产物。历史正文保留并标记过时,明确返回 `semantic_support:"not_scored"`。
56
+
57
+ 检索匹配标题、章节标题和整理后的正文,采用忽略大小写的字面词匹配(包含中文子串)及简单出现次数排序。仅保存 provider 摘录不会使它成为可检索的 Wiki 知识。没有 embedding 检索或语义重排,也不会未经实际问答评测就选择它们。
58
+
59
+ ## 质量评测输入
60
+
61
+ `evaluation.evaluate(suite, runs, judgments=None)` 只计算传入数据。完整可运行输入为 [research-quality.json](https://github.com/Browser-bookmark-hub/Bookmark-Research/blob/main/tests/fixtures/research-quality.json),源码和验证包也包含 `tests/fixtures/research-quality.json`。
62
+
63
+ - **suite** 定义任务、原始来源/问题 ID、金标准和版本化 rubric。gold_answers:null 表示没有答案集合基准。rubric 定义答案身份匹配、引用支持和 0–4 分的报告维度。
64
+ - **run** 记录 task、system、mode、repeat、最终报告、原子答案/claims/citations、覆盖 ID 和测量,并保留模型、工具、预算、输入快照/版本、时间窗口及宿主版本。引文按唯一 claim/source 对计算。
65
+ - **judgment** 标识 rubric 版本与真实人工/模型评审者,给出预测答案到 gold ID 的匹配、supported/unsupported/uncertain 引用标签及理由、报告维度分数及理由。合成样例的评审必须自报 synthetic。
66
+
67
+ 答案匹配是标注任务。评审者必须阅读真实预测,核对身份与范围;错误实体用 gold_id:null。评分器不猜别名,也不把正则匹配当语义裁判。多个预测匹配同一 gold 实体时,集合 precision/recall 只计算一次;未匹配预测算假阳性,重复匹配另行报告。
68
+
69
+ 每条引用判断针对完整命题及来源上下文,包括身份、日期和范围。supported 表示文本支持命题;unsupported 与 uncertain 都留在完整准确率分母中。原句匹配不能代替支持性判断。
70
+
71
+ ### 指标与缺失信息
72
+
73
+ | 输出 | 含义与边界 |
74
+ | --- | --- |
75
+ | 答案 precision、recall、F1 | 需要金标准与完整答案匹配,否则 null |
76
+ | 语义引用准确率 | 完整标注时为 supported/全部引文;局部标注另行显示 |
77
+ | 结论语义支持 | 全部引文已标注后,有 supported 引文的 claim/全部 claim |
78
+ | 引用存在率 | 有任意引文的 claim/全部 claim,仅是结构指标 |
79
+ | 报告质量 | 全部 rubric 维度已评分时,将提供的 0–4 分均值归一化 |
80
+ | accounted/text/reviewed 覆盖 | 各自相对原始来源集合计算,失败来源仍留在分母 |
81
+ | 问题覆盖 | 已记录回答的问题 ID/原始问题 ID,不判断答案真假 |
82
+ | 成本与时延 | 提供的美元/秒,注明 observed、reported 或 synthetic;未知保持 null |
83
+ | 重复运行波动 | 相同声明条件下的数量、均值、样本方差、样本标准差和极差 |
84
+
85
+ 覆盖必须满足 reviewed ⊆ text ⊆ accounted ⊆ original scope。报告的 ID 仍需证据审计,输入标签本身不证明实际阅读。插件无法取得的外部工具和模型用量保持未知。
86
+
87
+ 比较按同一任务分组。模型、工具、预算、输入版本、时间窗口、宿主、评审者或测量依据不同会被披露,并分开聚合。未知预算不能声称受控比较。只有一次观察时没有方差估计,缺失费用不会当 0 求均值。失败和未完成输出仍保留在运行集合中。
88
+
89
+ 输出固定 suite、run 和 judgment 哈希,保留评审者/rubric 身份及 null 分数原因。它不会生成 provider 排名、因果结论、RACE/FACT 分数或公共榜单分数。声称研究质量提高需要真实任务运行与可辩护的独立标注。
90
+
91
+ ## 重现离线演示
92
+
93
+ 以下命令需要源码或验证包;运行时导出带指南,但不含测试资料和验证脚本。
94
+
95
+ ```sh
96
+ python3 -B scripts/verify_quality.py
97
+ python3 -B -m unittest discover -s tests -p 'test_wiki.py'
98
+ python3 -B -m unittest discover -s tests -p 'test_evaluation.py'
99
+ ```
100
+
101
+ 验证脚本在数据目录的新运行目录下写入 inputs.json、evaluation.json 和可读的 evaluation.md。`--output /absolute/external/directory` 可另选目录;`--fixture /path/input.json` 接受含 suite、runs、judgments 和可选已知样例期望的包装对象。
102
+
103
+ 示例包含四条虚构来源、两个 gold 实体和六份手写输出,标成 direct、workflow、professional。一个来源不可用,部分输出遗漏或虚构答案,一个引文范围不确定。费用和时长是合成值,其中一个费用故意未知。这些情况验证分母、语义标签完整性、重复方差及缺失测量,**不测量这三条路线或任何真实宿主/模型/服务**。
104
+
105
+ Wiki 测试另覆盖不可变历史、索引发布失败、交叉链接、中文正文检索、来源拒绝/结论撤回、二手报告 provenance 及来源/包边界。真实宿主研究、专业服务比较和独立语义评审仍是另外的验证工作。
@@ -0,0 +1,13 @@
1
+ // Claude Dynamic Workflows supplies agent() and pipeline().
2
+ const host = {
3
+ name: "claude_code",
4
+ async all(jobs) {
5
+ const values = await pipeline(jobs, job => agent(job.prompt, {
6
+ label: job.key, schema: job.schema,
7
+ }));
8
+ return jobs.map((job, index) => ({
9
+ key: job.key, ok: values[index] != null, data: values[index] ?? null,
10
+ error: values[index] == null ? "Native workflow child failed or returned no result" : null,
11
+ }));
12
+ },
13
+ };
@@ -0,0 +1,14 @@
1
+ # Codex native research delegation
2
+
3
+ Use the native subagent tools visible in the current Codex session. This file explicitly requests delegation for a multi-group research task. It does not define a JavaScript runner or require custom global agents. Inherit the current model, reasoning, permissions and tool access unless the user specifies otherwise.
4
+
5
+ Resolve the output language from the user's explicit requirement, otherwise their actual task/conversation, with English as the fallback. Preserve it in the research brief and pass it to every reader, verifier and follow-up assignment. Use it for authored answers, report prose, review notes and Wiki titles/sections. Keep original quotations, URLs, IDs, code and technical field names unchanged; put translations outside quotations. This English instruction file does not select the answer language. The bilingual reading reference is in `docs/prompt-reference.en.md` and `docs/prompt-reference.md`.
6
+
7
+ 1. Read `research_status`. Resume an `incomplete` research explicitly with `research_record` kind `resume`; never reopen completed/cancelled research. Read every page of `research_inventory` without narrowing the frozen scope. The optional `python3 hosts/codex/prepare.py --research-id RID` helper calls the same shared MCP tools and emits exhaustive groups; it starts no agents. Keep every `u-` inventory ID and its original bookmark instances. A local package `source_id`, a `u-` inventory ID and an `sN` extracted source ID are different identifiers.
8
+ 2. Spawn one bounded reader per group within the native concurrency limit. Give each reader the research ID, exact assigned inventory IDs, method, relevant question IDs and a unique operation prefix. Readers call `research_inventory`, `research_fetch`/`research_search`, `research_source`, and `research_record`. Batch up to 8 needed URLs per fetch where applicable, respecting the existing `initial_fetch_plan` and budget. Require full relevant saved text, an explicit `source_review`, source-backed claims with exact quotes and a separate `inventory_review` for every assigned URL. Submit ready records through `research_record.entries` in batches of up to 50 with stable `batch_id` values under the reader's unique prefix. Place reviews before their claims; use returned claim IDs in subsequent inventory/answer batches. An invalid entry rejects the whole batch; identical retries reuse the batch ID. Return structured IDs, citations, judgments and failures. Failed fetches cannot become reviewed entries. Do not ask a child to create another scheduler.
9
+ 3. Wait for every reader through the native wait/agent controls. A pending thread or unread message is not completion. Preserve failed assignments and already-persisted progress. Then use separate, fresh verifier subagents for the same complete ID groups. Verifiers independently read claims and saved evidence, challenge unsupported conclusions and misleading freshness/completeness claims, and use retraction/gap/conflict/resolution records where justified. They must identify uncheckable items; a successful child turn is not proof of successful verification.
10
+ 4. Read `research_coverage` with `all`, `missing`, `unread` and `unreviewed`, following `next_offset` separately for every filter. Compare the complete inventory with every returned assignment, not just the first page or a count. Send the full difference plus failed verification IDs to another bounded reading and independent verification pass when needed. Use the host's current controls for child continuation or cancellation; no plugin lease or worker process exists.
11
+ 5. Save complete returned JSON with `research_record` kind `external_run`: include all inventory IDs, reader/verifier results, remaining IDs, actual observed states, and coverage metrics. Read the saved location from `recorded.result_path` and its digest from `recorded.result_sha256`. Use actual native run IDs when available. If only a caller correlation key is available, prefix it with `local:` and say it is not a native host run ID. Record only completed observations as completed; an unknown outcome is not cancellation.
12
+ 6. Synthesize answers with valid claim IDs and run coverage again. `research_finish` owns the completion gate. Unverified major conclusions, unexplained errors or remaining coverage gaps require an `incomplete` report. The parent must directly read `research_status` and every relevant `research_coverage` page after child reports settle, compare returned paths with actual `artifacts` and `external_runs`, and read the complete analysis artifact with its verified SHA-256. A child's success or claimed path is not confirmation. Link the verified report, full saved result and coverage, and retain excluded/blocked sources and their original bookmark context. Do not retry an unknown paid outcome automatically.
13
+
14
+ If native subagent tools are disabled or unavailable, report the missing capability. The host may perform a bounded single-agent task under the same research rules; do not claim native delegation occurred. Native subagent threads and plugin research archives have separate lifecycles.
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env python3
2
+ """Prepare exhaustive native Codex assignments; this script never spawns agents."""
3
+
4
+ import argparse
5
+ import importlib.util
6
+ import json
7
+ from pathlib import Path
8
+ import sys
9
+
10
+
11
+ def main():
12
+ for stream in (sys.stdout, sys.stderr): # Windows pipes default to the ANSI code page
13
+ if hasattr(stream, "reconfigure"):
14
+ stream.reconfigure(encoding="utf-8")
15
+ parser = argparse.ArgumentParser(description=__doc__)
16
+ parser.add_argument("--research-id", required=True)
17
+ parser.add_argument("--group-size", type=int, default=12)
18
+ parser.add_argument("--db")
19
+ parser.add_argument("--config")
20
+ options = parser.parse_args()
21
+ if not 1 <= options.group_size <= 50:
22
+ parser.error("group-size must be 1..50")
23
+ root = Path(__file__).resolve().parents[2]
24
+ spec = importlib.util.spec_from_file_location("research_call", root / "hosts/shared/research-call.py")
25
+ bridge = importlib.util.module_from_spec(spec)
26
+ spec.loader.exec_module(bridge)
27
+ items = []
28
+ offset = 0
29
+ seen_offsets = set()
30
+ total = None
31
+ try:
32
+ while offset is not None:
33
+ if offset in seen_offsets:
34
+ raise ValueError("Inventory pagination repeated an offset")
35
+ seen_offsets.add(offset)
36
+ response = bridge.call_tool({"name": "research_inventory", "arguments": {
37
+ "research_id": options.research_id, "offset": offset, "limit": 100}}, options.db, options.config)
38
+ if response["isError"]:
39
+ raise ValueError(json.dumps(response["result"], ensure_ascii=False))
40
+ page = response["result"]
41
+ if total is not None and total != page["total"]:
42
+ raise ValueError("Frozen inventory total changed while reading")
43
+ total = page["total"]
44
+ items.extend(page["items"])
45
+ offset = page["next_offset"]
46
+ ids = [row["id"] for row in items]
47
+ if len(ids) != total or len(set(ids)) != total:
48
+ raise ValueError("Inventory pages did not contain every unique source ID")
49
+ groups = [{"group": index // options.group_size, "inventory_ids": ids[index:index + options.group_size]}
50
+ for index in range(0, len(ids), options.group_size)]
51
+ print(json.dumps({"research_id": options.research_id, "total": total, "inventory_ids": ids,
52
+ "groups": groups, "instructions": str(root / "hosts/codex/delegate.md"),
53
+ "agents_started": 0}, ensure_ascii=False, indent=2))
54
+ except (OSError, ValueError, RuntimeError, KeyError) as error:
55
+ print(json.dumps({"error": str(error)}, ensure_ascii=False), file=sys.stderr)
56
+ return 1
57
+ return 0
58
+
59
+
60
+ if __name__ == "__main__":
61
+ raise SystemExit(main())
@@ -0,0 +1,36 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ export const name = "bookmark-research";
5
+ export const inject = ["skills"];
6
+
7
+ // The module travels with the Python runtime; no exporting machine's path is embedded.
8
+ const root = fileURLToPath(new URL("../../", import.meta.url));
9
+ const skillUrl = new URL("../../skills/bookmark-research/SKILL.md", import.meta.url);
10
+ const resourceBase = { kind: "directory", path: fileURLToPath(new URL("./", skillUrl)) };
11
+ const candidate = {
12
+ name,
13
+ description: "Research bookmark URLs or Bookmark Canvas packages with shared Python tools and retained evidence.",
14
+ invocation: { modelInvocable: true, userInvocable: true },
15
+ provider: name,
16
+ source: "bundled",
17
+ resourceBase,
18
+ path: fileURLToPath(skillUrl),
19
+ rank: 600,
20
+ locator: skillUrl,
21
+ };
22
+
23
+ export function apply(ctx) {
24
+ ctx.provide("bookmarkResearchPaths", {
25
+ root,
26
+ cli: fileURLToPath(new URL("../../src/cli.py", import.meta.url)),
27
+ });
28
+ ctx.skills.registerProvider(() => ({
29
+ name,
30
+ async list() { return [candidate]; },
31
+ async get() {
32
+ const content = (await readFile(skillUrl, "utf8")).replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
33
+ return { ...candidate, content };
34
+ },
35
+ }));
36
+ }
@@ -0,0 +1,14 @@
1
+ // DSH supplies these hooks through its configured workflow engine.
2
+ // Fatal hook/schema/cap errors propagate; ordinary child failures remain visible.
3
+ const host = {
4
+ name: "dsh",
5
+ async all(jobs) {
6
+ const values = await pipeline(jobs, job => agent(job.prompt, {
7
+ label: job.key, schema: job.schema,
8
+ }));
9
+ return jobs.map((job, index) => ({
10
+ key: job.key, ok: values[index] != null, data: values[index] ?? null,
11
+ error: values[index] == null ? "Native workflow child failed or returned no result" : null,
12
+ }));
13
+ },
14
+ };
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env python3
2
+ """Print a complete DSH workflow tool call; never start the workflow here."""
3
+
4
+ import argparse
5
+ import json
6
+ from pathlib import Path
7
+ import sys
8
+
9
+
10
+ def main():
11
+ for stream in (sys.stdout, sys.stderr): # Windows pipes default to the ANSI code page
12
+ if hasattr(stream, "reconfigure"):
13
+ stream.reconfigure(encoding="utf-8")
14
+ parser = argparse.ArgumentParser(description=__doc__)
15
+ parser.add_argument("--research-id", required=True)
16
+ parser.add_argument("--run-key", required=True)
17
+ parser.add_argument("--group-size", type=int, default=12)
18
+ parser.add_argument("--max-gap-rounds", type=int, default=1)
19
+ parser.add_argument("--method", default="comparison")
20
+ parser.add_argument("--output-language", default="auto",
21
+ help="Output language name/code; auto follows the brief/questions, with English as fallback")
22
+ options = parser.parse_args()
23
+ if not 1 <= options.group_size <= 50 or not 0 <= options.max_gap_rounds <= 4:
24
+ parser.error("group-size must be 1..50 and max-gap-rounds must be 0..4")
25
+ if (not options.output_language.strip() or len(options.output_language) > 80
26
+ or any(ord(char) < 32 or ord(char) == 127 for char in options.output_language)):
27
+ parser.error("output-language must be 1..80 characters without control characters")
28
+ options.output_language = options.output_language.strip()
29
+ root = Path(__file__).resolve().parents[2]
30
+ directory = root / "workflows/bookmark-research"
31
+ if directory.is_dir():
32
+ meta = json.loads((directory / "meta.json").read_text(encoding="utf-8"))
33
+ script = (directory / "script.js").read_text(encoding="utf-8")
34
+ else:
35
+ sys.path.insert(0, str(root / "scripts"))
36
+ from host_assets import build_workflow
37
+ definition, script = build_workflow(root, "dsh")
38
+ meta = definition["meta"]
39
+ args = vars(options)
40
+ args["bridge_path"] = str(root / "hosts/shared/research-call.py")
41
+ print(json.dumps({"meta": meta, "script": script, "args": args}, ensure_ascii=False, indent=2))
42
+
43
+
44
+ if __name__ == "__main__":
45
+ main()
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env python3
2
+ """Register the saved workflow in one project, without changing Pi settings."""
3
+
4
+ import argparse
5
+ import json
6
+ from pathlib import Path
7
+ import sys
8
+ import tempfile
9
+
10
+
11
+ def register(project):
12
+ project = Path(project).expanduser().absolute()
13
+ if not project.is_dir() or project.is_symlink():
14
+ raise ValueError("Project must be an existing, nonsymlink directory")
15
+ project = project.resolve()
16
+ root = Path(__file__).resolve().parents[2]
17
+ source = root / "workflows/bookmark-research"
18
+ if source.is_dir():
19
+ manifest = json.loads((source / "workflow.json").read_text(encoding="utf-8"))
20
+ script = (source / "script.js").read_bytes()
21
+ else:
22
+ sys.path.insert(0, str(root / "scripts"))
23
+ from host_assets import build_workflow
24
+ definition, script = build_workflow(root, "pi")
25
+ manifest = {**definition["meta"], "parameters": definition["parameters"]}
26
+ script = script.encode("utf-8")
27
+ manifest["parameters"]["bridge_path"] = {
28
+ "type": "string", "default": str(root / "hosts/shared/research-call.py"),
29
+ "description": "Shared research tool bridge in this stable bundle location",
30
+ }
31
+ target = project / ".pi/subagent-workflows/bookmark-research"
32
+ current = project
33
+ for part in target.relative_to(project).parts:
34
+ current /= part
35
+ if current.is_symlink():
36
+ raise ValueError("Refusing a symlink registration path: " + str(current))
37
+ payload = {"workflow.json": (json.dumps(manifest, ensure_ascii=False, indent=2) + "\n").encode("utf-8"),
38
+ "script.js": script}
39
+ if target.exists():
40
+ same = target.is_dir() and {p.name for p in target.iterdir()} == set(payload) and all(
41
+ (target / name).is_file() and not (target / name).is_symlink() and
42
+ (target / name).read_bytes() == content for name, content in payload.items())
43
+ if not same:
44
+ raise ValueError("Existing workflow differs; preserve it and choose a clean project registration location")
45
+ return {"path": str(target), "changed": False, "extensions_installed": False}
46
+ target.parent.mkdir(parents=True, exist_ok=True)
47
+ with tempfile.TemporaryDirectory(prefix=".bookmark-workflow-", dir=target.parent) as temporary:
48
+ stage = Path(temporary) / "bookmark-research"
49
+ stage.mkdir()
50
+ for name, content in payload.items():
51
+ (stage / name).write_bytes(content)
52
+ stage.rename(target)
53
+ return {"path": str(target), "changed": True, "extensions_installed": False}
54
+
55
+
56
+ def main():
57
+ for stream in (sys.stdout, sys.stderr): # Windows pipes default to the ANSI code page
58
+ if hasattr(stream, "reconfigure"):
59
+ stream.reconfigure(encoding="utf-8")
60
+ parser = argparse.ArgumentParser(description=__doc__)
61
+ parser.add_argument("--project", required=True, help="Existing project whose .pi/subagent-workflows directory to use")
62
+ options = parser.parse_args()
63
+ try:
64
+ print(json.dumps(register(options.project), ensure_ascii=False, indent=2))
65
+ except (OSError, ValueError) as error:
66
+ print(json.dumps({"error": str(error)}, ensure_ascii=False), file=sys.stderr)
67
+ return 1
68
+ return 0
69
+
70
+
71
+ if __name__ == "__main__":
72
+ raise SystemExit(main())
@@ -0,0 +1,18 @@
1
+ // pi-subagents-workflows injects args/cwd; pi-subagents owns runs and lifecycle.
2
+ const host = {
3
+ name: "pi",
4
+ all(jobs) {
5
+ return runs.all(jobs.map(job => ({
6
+ key: job.key, agent: "delegate", task: job.prompt, cwd,
7
+ context: "fresh", outputSchema: job.schema,
8
+ }))).then(values => jobs.map((job, index) => {
9
+ const run = values[index];
10
+ const ok = Boolean(run && !run.error && run.ok !== false && run.structuredOutput);
11
+ return {
12
+ key: job.key, ok, data: ok ? run.structuredOutput : null,
13
+ error: ok ? null : String(run?.error || "Missing structuredOutput or failed child"),
14
+ child_run_id: run?.runId || null,
15
+ };
16
+ }));
17
+ },
18
+ };
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ """Call one shared research MCP tool using JSON stdin and the bundled server."""
3
+
4
+ import argparse
5
+ import json
6
+ from pathlib import Path
7
+ import subprocess
8
+ import sys
9
+
10
+
11
+ def call_tool(request, db=None, config=None, timeout=600):
12
+ if not isinstance(request, dict) or set(request) != {"name", "arguments"}:
13
+ raise ValueError("Request must contain exactly name and arguments")
14
+ if not isinstance(request["name"], str) or not request["name"].startswith("research_"):
15
+ raise ValueError("This bridge calls research_* tools only")
16
+ if not isinstance(request["arguments"], dict):
17
+ raise ValueError("arguments must be a JSON object")
18
+ command = [sys.executable, "-B", str(Path(__file__).resolve().parents[2] / "src/cli.py")]
19
+ if db:
20
+ command.extend(["--db", str(db)])
21
+ if config:
22
+ command.extend(["--config", str(config)])
23
+ command.append("serve")
24
+ messages = [
25
+ {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
26
+ "protocolVersion": "2025-11-25", "capabilities": {},
27
+ "clientInfo": {"name": "bookmark-research-host-bridge", "version": "1"}}},
28
+ {"jsonrpc": "2.0", "method": "notifications/initialized"},
29
+ {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": request},
30
+ ]
31
+ result = subprocess.run(command, input="\n".join(json.dumps(row) for row in messages) + "\n",
32
+ text=True, encoding="utf-8", errors="replace", capture_output=True, timeout=timeout)
33
+ if result.returncode:
34
+ raise RuntimeError("Shared MCP process failed: " + result.stderr.strip()[:2000])
35
+ responses = [json.loads(line) for line in result.stdout.splitlines() if line.strip()]
36
+ response = next((row for row in responses if row.get("id") == 2), None)
37
+ if response is None:
38
+ raise RuntimeError("Shared MCP process returned no tool response")
39
+ if "error" in response:
40
+ return {"isError": True, "result": response["error"]}
41
+ tool_result = response["result"]
42
+ if "structuredContent" in tool_result:
43
+ value = tool_result["structuredContent"]
44
+ else:
45
+ text = "\n".join(item["text"] for item in tool_result.get("content", []) if item.get("type") == "text")
46
+ try:
47
+ value = json.loads(text)
48
+ except json.JSONDecodeError:
49
+ value = {"text": text}
50
+ return {"isError": bool(tool_result.get("isError")), "result": value}
51
+
52
+
53
+ def main():
54
+ for stream in (sys.stdout, sys.stderr): # Windows pipes default to the ANSI code page
55
+ if hasattr(stream, "reconfigure"):
56
+ stream.reconfigure(encoding="utf-8")
57
+ parser = argparse.ArgumentParser(description=__doc__)
58
+ parser.add_argument("--db")
59
+ parser.add_argument("--config")
60
+ parser.add_argument("--timeout", type=float, default=600)
61
+ options = parser.parse_args()
62
+ try:
63
+ if options.timeout <= 0:
64
+ raise ValueError("timeout must be positive")
65
+ result = call_tool(json.load(sys.stdin), options.db, options.config, options.timeout)
66
+ except subprocess.TimeoutExpired:
67
+ result = {"isError": True, "unknown_outcome": True,
68
+ "result": {"error": "MCP call timed out. Inspect research_status before retrying; an operation may have reserved budget or saved a result."}}
69
+ except (OSError, ValueError, RuntimeError) as error:
70
+ result = {"isError": True, "result": {"error": str(error)}}
71
+ print(json.dumps(result, ensure_ascii=False))
72
+ return 1 if result["isError"] else 0
73
+
74
+
75
+ if __name__ == "__main__":
76
+ raise SystemExit(main())