psyclaw 0.28.2 → 0.29.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 (45) hide show
  1. package/README.md +8 -5
  2. package/agents/recommended/catalog.json +1 -1
  3. package/dist/src/adapters/pi/extension.js +191 -84
  4. package/dist/src/adapters/pi/extension.js.map +1 -1
  5. package/dist/src/analysis/hooks.js +1 -1
  6. package/dist/src/analysis/hooks.js.map +1 -1
  7. package/dist/src/ars/doctor.d.ts +27 -0
  8. package/dist/src/ars/doctor.js +121 -0
  9. package/dist/src/ars/doctor.js.map +1 -0
  10. package/dist/src/ars/keybindings.d.ts +13 -0
  11. package/dist/src/ars/keybindings.js +50 -0
  12. package/dist/src/ars/keybindings.js.map +1 -0
  13. package/dist/src/ars/mode-editor.d.ts +17 -5
  14. package/dist/src/ars/mode-editor.js +55 -18
  15. package/dist/src/ars/mode-editor.js.map +1 -1
  16. package/dist/src/ars/profile.d.ts +10 -0
  17. package/dist/src/ars/profile.js +15 -2
  18. package/dist/src/ars/profile.js.map +1 -1
  19. package/dist/src/branding.js +12 -13
  20. package/dist/src/branding.js.map +1 -1
  21. package/dist/src/chat.js +3 -0
  22. package/dist/src/chat.js.map +1 -1
  23. package/dist/src/index.d.ts +4 -0
  24. package/dist/src/index.js +4 -0
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/panel/server.js +1 -1
  27. package/dist/src/panel/server.js.map +1 -1
  28. package/dist/src/project/bootstrap.d.ts +7 -2
  29. package/dist/src/project/bootstrap.js +14 -13
  30. package/dist/src/project/bootstrap.js.map +1 -1
  31. package/dist/src/project/paths.d.ts +9 -0
  32. package/dist/src/project/paths.js +18 -3
  33. package/dist/src/project/paths.js.map +1 -1
  34. package/dist/src/project/workspace.d.ts +14 -0
  35. package/dist/src/project/workspace.js +82 -0
  36. package/dist/src/project/workspace.js.map +1 -0
  37. package/dist/src/session/modes.d.ts +10 -0
  38. package/dist/src/session/modes.js +52 -0
  39. package/dist/src/session/modes.js.map +1 -0
  40. package/dist/src/verify/checklist.d.ts +20 -0
  41. package/dist/src/verify/checklist.js +71 -0
  42. package/dist/src/verify/checklist.js.map +1 -0
  43. package/package.json +1 -1
  44. package/vendor/ars/pi/wrapper.js +14 -5
  45. package/vendor/ars/pi/wrapper.test.mjs +10 -0
package/README.md CHANGED
@@ -4,7 +4,7 @@ PsyClaw 是面向社会科学研究的智能体工作台。它把研究项目、
4
4
 
5
5
  PsyClaw 自有代码使用 MIT 许可证;随 npm 包内置的 Academic Research Skills 位于 `vendor/ars`,保持上游署名并单独遵循 CC BY-NC 4.0,仅限非商业用途。
6
6
 
7
- 当前版本:`0.28.2`。正式命令、用户配置目录和后续发布统一使用 `psyclaw`。
7
+ 当前版本:`0.29.1`。正式命令、用户配置目录和后续发布统一使用 `psyclaw`。
8
8
 
9
9
  ## 发布流程
10
10
 
@@ -21,19 +21,19 @@ RELEASE_MESSAGE="release: describe the change" pnpm release:push
21
21
  需要 Node.js `>=22.19.0`。官方 npm 源:
22
22
 
23
23
  ```powershell
24
- npm install -g psyclaw@0.28.2
24
+ npm install -g psyclaw@0.29.1
25
25
  ```
26
26
 
27
27
  如果本机 npm 配置把 registry 误写成带有 `~/` 的地址,请显式指定官方源:
28
28
 
29
29
  ```bash
30
- npm install -g psyclaw@0.28.2 --registry=https://registry.npmjs.org/
30
+ npm install -g psyclaw@0.29.1 --registry=https://registry.npmjs.org/
31
31
  ```
32
32
 
33
33
  中国大陆网络较慢或无法访问官方源时:
34
34
 
35
35
  ```powershell
36
- npm install -g psyclaw@0.28.2 --registry=https://registry.npmmirror.com
36
+ npm install -g psyclaw@0.29.1 --registry=https://registry.npmmirror.com
37
37
  ```
38
38
 
39
39
  PsyClaw 的 npm 发布包直接携带 Windows x64/arm64 版 `ripgrep` 和 `fd`,Windows 启动时直接使用包内可执行文件,不再联网下载。其他平台缺少这两个工具时仍由锁定的 Pi 原生工具管理器处理:代理环境走 GitHub 官方 API 与 Release,国内 npm registry 环境统一走国内镜像。
@@ -92,7 +92,10 @@ psyclaw brief
92
92
  | 创建项目能力 | `/create-skill`、`/create-hook`、`/create-rule`、`/create-subagent`(均先预览并确认) |
93
93
  | 运行自定义只读角色 | `/agents --agent <id> <task>` 或 `/agents --agents <id,...> <task>`(最多四个隔离 worker) |
94
94
  | 调用已加载 Skill | `/skill:<name>` |
95
- | 使用 PsyClaw ARS | 输入 `ars` 后按 Tab(或 `ars: <task>`);也可用 `/ars doctor` / `/ars full <task>` |
95
+ | 使用模式 | `Shift+Tab`:`chat` `analysis` `academic`;thinking `Ctrl+Shift+Tab` |
96
+ | 搭建仓库 | `/init`(只建目录 + `psyclaw.md`) |
97
+ | 人确认关键字段 | `/verify list` 或 `/verify <id> verified` |
98
+ | 使用 PsyClaw ARS | 切到 `academic`;也可用 `/ars doctor` / `/ars full <task>` |
96
99
  | 管理 Plugin | `/plugin`;终端使用 `psyclaw plugin install|remove|list` |
97
100
  | 调用已配置 MCP | 模型通过 `psyclaw_mcp` 自动发现并调用 `.psyclaw/mcp/*.json` 中启用的服务器 |
98
101
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "psyclaw/recommended-agents/v1",
3
- "documentVersion": "0.28.2",
3
+ "documentVersion": "0.29.1",
4
4
  "syncedAt": "2026-09-06",
5
5
  "sourceDocument": "bundled literature/review subagents",
6
6
  "sourceRef": "bundled",
@@ -20,8 +20,12 @@ import { SkillManagerComponent } from "../../tui/skill-manager.js";
20
20
  import { enabledLocalSkillPaths, enabledLocalPromptPaths, readUserSkillState, scanLocalSkills, setLocalSkillEnabled, setLocalSkillsEnabled, skillNamesInPaths, installLocalSkill, userSkillId, } from "../../skills/user-skills.js";
21
21
  import { RuntimeMcpRegistry, setUserMcpConfigEnabled, } from "../../integrations/mcp-runtime.js";
22
22
  import { SecretInputComponent, ProviderPickerComponent, } from "../../tui/provider-picker.js";
23
- import { ARS_REPOSITORY_URL, ARS_UPSTREAM_COMMIT, ARS_UPSTREAM_REF, PSYCLAW_ARS_PROFILE_VERSION, detectNatureArsFillers, formatNatureArsFillerStatus, isArsPiActive, isArsPiTurn, psyclawArsPatch, } from "../../ars/profile.js";
24
- import { ArsModeEditor, enterArsModeEditorText, isArsModeEditorText } from "../../ars/mode-editor.js";
23
+ import { ARS_REPOSITORY_URL, ARS_UPSTREAM_COMMIT, ARS_UPSTREAM_REF, PSYCLAW_ARS_PROFILE_VERSION, detectNatureArsFillers, formatNatureArsFillerStatus, isArsPiActive, isArsPiTurn, psyclawArsPatch, setArsPiSessionActive, } from "../../ars/profile.js";
24
+ import { buildArsDoctorReport, ensurePdfEngineForExport } from "../../ars/doctor.js";
25
+ import { arsRoot } from "../../ars/pi-panel-executor.js";
26
+ import { ArsModeEditor, ARS_MODE_STATUS, isArsModeEditorText } from "../../ars/mode-editor.js";
27
+ import { MODE_STATUS, nextSessionMode, parseSessionMode, sessionModePrompt, } from "../../session/modes.js";
28
+ import { formatVerifyChecklist, loadVerifyChecklist, markVerifyItem } from "../../verify/checklist.js";
25
29
  const PARADIGMS = new Set([
26
30
  "survey-observational",
27
31
  "qualitative-thematic",
@@ -36,19 +40,19 @@ const PARADIGMS = new Set([
36
40
  ]);
37
41
  function parseInitArgs(args) {
38
42
  const trimmed = args.trim();
43
+ if (!trimmed)
44
+ return {};
39
45
  const match = trimmed.match(/^--paradigm(?:=|\s+)(\S+)(?:\s+([\s\S]*))?$/i);
40
46
  if (trimmed.startsWith("--")) {
41
47
  const paradigm = match?.[1];
42
48
  const goal = match?.[2]?.trim();
43
- if (!match || !paradigm || !goal) {
44
- throw new Error("Usage: /init [--paradigm survey-observational] <research goal>");
49
+ if (!match || !paradigm) {
50
+ throw new Error("Usage: /init [--paradigm survey-observational] [optional goal]");
45
51
  }
46
52
  if (!PARADIGMS.has(paradigm))
47
53
  throw new Error(`Unsupported paradigm: ${paradigm}`);
48
- return { paradigm, goal };
54
+ return { paradigm, ...(goal ? { goal } : {}) };
49
55
  }
50
- if (!trimmed)
51
- throw new Error("Usage: /init [--paradigm survey-observational] <research goal>");
52
56
  return { paradigm: "survey-observational", goal: trimmed };
53
57
  }
54
58
  async function notifyError(ctx, error) {
@@ -1118,6 +1122,40 @@ export default function psyclawExtension(pi) {
1118
1122
  const developerCommands = process.env.PSYCLAW_DEVELOPER_COMMANDS === "1";
1119
1123
  const legacyTestApi = typeof pi.registerTool !== "function";
1120
1124
  const runtimeMcps = new RuntimeMcpRegistry();
1125
+ let arsModeEditor;
1126
+ let arsUiContext;
1127
+ let sessionMode = "chat";
1128
+ const applySessionMode = (mode, opts) => {
1129
+ sessionMode = mode;
1130
+ arsModeEditor?.setMode(mode);
1131
+ arsUiContext?.ui.setStatus("mode", MODE_STATUS[mode]);
1132
+ // Clear legacy ars status key when leaving academic.
1133
+ if (mode !== "academic")
1134
+ arsUiContext?.ui.setStatus("ars", undefined);
1135
+ else
1136
+ arsUiContext?.ui.setStatus("ars", ARS_MODE_STATUS);
1137
+ if (opts?.syncSession === false || !arsUiContext)
1138
+ return;
1139
+ setArsPiSessionActive(typeof pi.appendEntry === "function"
1140
+ ? pi.appendEntry.bind(pi)
1141
+ : undefined, mode === "academic");
1142
+ if (typeof pi.appendEntry === "function") {
1143
+ pi.appendEntry("psyclaw-session-mode", { mode });
1144
+ }
1145
+ };
1146
+ const cycleSessionMode = () => {
1147
+ const next = nextSessionMode(arsModeEditor?.getMode() ?? sessionMode);
1148
+ applySessionMode(next);
1149
+ arsUiContext?.ui.setStatus("mode", MODE_STATUS[next] ?? "chat");
1150
+ };
1151
+ if (!legacyTestApi && typeof pi.registerShortcut === "function") {
1152
+ pi.registerShortcut("shift+tab", {
1153
+ description: "Cycle chat → analysis → academic",
1154
+ handler: () => {
1155
+ cycleSessionMode();
1156
+ },
1157
+ });
1158
+ }
1121
1159
  if (!legacyTestApi && typeof pi.on === "function")
1122
1160
  pi.on("resources_discover", async (event) => {
1123
1161
  const enabled = await enabledRecommendedSkillPaths(event.cwd);
@@ -1144,24 +1182,48 @@ export default function psyclawExtension(pi) {
1144
1182
  });
1145
1183
  if (!legacyTestApi && typeof pi.on === "function")
1146
1184
  pi.on("before_agent_start", (event, ctx) => {
1147
- if (!isArsPiTurn(event.systemPrompt) && !isArsPiActive(ctx?.sessionManager))
1148
- return;
1149
- return {
1150
- systemPrompt: `${event.systemPrompt}\n\n${psyclawArsPatch({
1151
- skills: event.systemPromptOptions?.skills,
1152
- systemPrompt: event.systemPrompt,
1153
- })}`,
1154
- };
1185
+ const modeEntry = [...(ctx?.sessionManager?.getBranch?.() ?? [])].reverse().find((entry) => entry.type === "custom" && entry.customType === "psyclaw-session-mode");
1186
+ const restored = parseSessionMode(modeEntry?.data?.mode)
1187
+ ?? (isArsPiActive(ctx?.sessionManager) ? "academic" : sessionMode);
1188
+ sessionMode = restored;
1189
+ const modeBlock = sessionModePrompt(restored);
1190
+ if (restored === "academic" || isArsPiTurn(event.systemPrompt) || isArsPiActive(ctx?.sessionManager)) {
1191
+ return {
1192
+ systemPrompt: `${event.systemPrompt}\n\n${modeBlock}\n\n${psyclawArsPatch({
1193
+ skills: event.systemPromptOptions?.skills,
1194
+ systemPrompt: event.systemPrompt,
1195
+ })}`,
1196
+ };
1197
+ }
1198
+ return { systemPrompt: `${event.systemPrompt}\n\n${modeBlock}` };
1155
1199
  });
1156
1200
  if (!legacyTestApi && typeof pi.on === "function")
1157
1201
  pi.on("session_start", (_event, ctx) => {
1202
+ arsUiContext = ctx;
1158
1203
  if (!ctx.hasUI || typeof ctx.ui.setEditorComponent !== "function")
1159
1204
  return;
1160
1205
  ctx.ui.setEditorComponent((tui, theme, keybindings) => {
1161
1206
  const editor = new ArsModeEditor(tui, theme, keybindings);
1162
- editor.onArsModeChange = (active) => {
1163
- ctx.ui.setStatus("ars", active ? "ARS" : undefined);
1207
+ arsModeEditor = editor;
1208
+ editor.onModeChange = (mode) => {
1209
+ sessionMode = mode;
1210
+ ctx.ui.setStatus("mode", MODE_STATUS[mode]);
1211
+ if (mode === "academic")
1212
+ ctx.ui.setStatus("ars", ARS_MODE_STATUS);
1213
+ else
1214
+ ctx.ui.setStatus("ars", undefined);
1164
1215
  };
1216
+ editor.onCycleMode = () => {
1217
+ cycleSessionMode();
1218
+ };
1219
+ const modeEntry = [...ctx.sessionManager.getBranch()].reverse().find((entry) => entry.type === "custom" && entry.customType === "psyclaw-session-mode");
1220
+ const restore = parseSessionMode(modeEntry?.data?.mode)
1221
+ ?? (isArsPiActive(ctx.sessionManager) ? "academic" : "chat");
1222
+ editor.setMode(restore);
1223
+ sessionMode = restore;
1224
+ ctx.ui.setStatus("mode", MODE_STATUS[restore]);
1225
+ if (restore === "academic")
1226
+ ctx.ui.setStatus("ars", ARS_MODE_STATUS);
1165
1227
  return editor;
1166
1228
  });
1167
1229
  });
@@ -1174,21 +1236,6 @@ export default function psyclawExtension(pi) {
1174
1236
  ctx.ui.notify("PsyClaw 已统一隐藏底层登录命令。请使用 /provider 配置、切换或更新模型凭据。", "info");
1175
1237
  return { action: "handled" };
1176
1238
  });
1177
- if (!legacyTestApi && typeof pi.on === "function")
1178
- pi.on("input", async (event, ctx) => {
1179
- if (event.source === "extension" || isArsPiActive(ctx.sessionManager))
1180
- return;
1181
- const text = event.text.trim();
1182
- const entersArs = isArsModeEditorText(text)
1183
- || /^\/(?:ars-[a-z0-9-]+|skill:(?:deep-research|academic-paper|academic-paper-reviewer|academic-pipeline))(?:\s|$)/i.test(text);
1184
- if (!entersArs)
1185
- return;
1186
- if (!ctx.hasUI)
1187
- return { action: "handled" };
1188
- const approved = await ctx.ui.confirm("启用 ARS?", `来源:${ARS_REPOSITORY_URL} @ ${ARS_UPSTREAM_REF}\n许可:CC BY-NC 4.0,仅限非商业用途。关键研究阶段仍会请求确认。`);
1189
- if (!approved)
1190
- return { action: "handled" };
1191
- });
1192
1239
  if (!legacyTestApi && typeof pi.on === "function")
1193
1240
  pi.on("tool_call", async (event, ctx) => {
1194
1241
  const run = await readControlledRun(ctx.cwd);
@@ -1223,18 +1270,42 @@ export default function psyclawExtension(pi) {
1223
1270
  return undefined;
1224
1271
  });
1225
1272
  pi.registerCommand("init", {
1226
- description: "初始化可追溯的研究项目",
1273
+ description: "搭建干净工作仓库(目录 + psyclaw.md),不自动追问或启动 /run",
1227
1274
  handler: async (args, ctx) => {
1228
1275
  try {
1229
1276
  const parsed = parseInitArgs(args);
1230
1277
  const project = await bootstrapProject({ root: ctx.cwd, ...parsed });
1231
- if (typeof pi.sendUserMessage === "function") {
1232
- pi.sendUserMessage(academicGrillRequest(project.goal, "init"), ctx.isIdle() ? {} : { deliverAs: "followUp" });
1233
- ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm})。当前保持普通对话;如需进入受控研究流程,请明确运行 /run。`, "info");
1278
+ ctx.ui.notify([
1279
+ `工作仓库已初始化:${project.id}`,
1280
+ "已创建 data/raw|clean、analysis/、literature/、paper/、psyclaw.md、.psyclaw/",
1281
+ "Shift+Tab:chat → analysis → academic",
1282
+ "澄清与分析请切到 analysis;写作审稿请切到 academic。可选 /run 加强受控工具。",
1283
+ ].join("\n"), "info");
1284
+ }
1285
+ catch (error) {
1286
+ await notifyError(ctx, error);
1287
+ }
1288
+ },
1289
+ });
1290
+ pi.registerCommand("verify", {
1291
+ description: "人确认关键字段:/verify list | /verify <id> verified|unverified|flagged",
1292
+ handler: async (args, ctx) => {
1293
+ try {
1294
+ const parts = args.trim().split(/\s+/).filter(Boolean);
1295
+ if (parts.length === 0 || parts[0] === "list") {
1296
+ const checklist = await loadVerifyChecklist(ctx.cwd);
1297
+ ctx.ui.notify(formatVerifyChecklist(checklist), "info");
1298
+ return;
1234
1299
  }
1235
- else {
1236
- ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm})。使用 /run 启动受控研究流程。`, "info");
1300
+ const id = parts[0];
1301
+ const status = (parts[1] ?? "verified");
1302
+ if (status !== "verified" && status !== "unverified" && status !== "flagged") {
1303
+ ctx.ui.notify("Usage: /verify list | /verify <id> verified|unverified|flagged [备注]", "info");
1304
+ return;
1237
1305
  }
1306
+ const notes = parts.slice(2).join(" ") || undefined;
1307
+ const checklist = await markVerifyItem(ctx.cwd, id, status, notes);
1308
+ ctx.ui.notify(formatVerifyChecklist(checklist), "info");
1238
1309
  }
1239
1310
  catch (error) {
1240
1311
  await notifyError(ctx, error);
@@ -1252,7 +1323,7 @@ export default function psyclawExtension(pi) {
1252
1323
  project = await readProject(ctx.cwd);
1253
1324
  }
1254
1325
  catch {
1255
- ctx.ui.notify("当前目录还没有通过 /init 建立的研究记录,因此保持普通对话模式。需要受控研究流程时,请先运行 /init,再运行 /run。", "warning");
1326
+ ctx.ui.notify("当前目录还没有通过 /init 建立的研究记录。可先 /init 建仓;需要更密受控工具链时再 /run。", "warning");
1256
1327
  return;
1257
1328
  }
1258
1329
  const available = await selectableRunSkills(ctx.cwd);
@@ -1279,9 +1350,9 @@ export default function psyclawExtension(pi) {
1279
1350
  }
1280
1351
  },
1281
1352
  });
1282
- if (developerCommands || legacyTestApi)
1283
- pi.registerCommand("verify", {
1284
- description: "写入机器可读的交接检查点",
1353
+ if (developerCommands)
1354
+ pi.registerCommand("handoff", {
1355
+ description: "(开发)写入机器可读交接检查点",
1285
1356
  handler: async (_args, ctx) => {
1286
1357
  try {
1287
1358
  const paths = projectPaths(ctx.cwd);
@@ -1297,7 +1368,7 @@ export default function psyclawExtension(pi) {
1297
1368
  verificationCommands: ["pnpm typecheck", "pnpm test"],
1298
1369
  generatedAt: new Date().toISOString(),
1299
1370
  });
1300
- ctx.ui.notify("Wrote notes/HANDOFF.md and notes/handoff.json", "info");
1371
+ ctx.ui.notify("Wrote .psyclaw/notes/HANDOFF.md and handoff.json", "info");
1301
1372
  }
1302
1373
  catch (error) {
1303
1374
  await notifyError(ctx, error);
@@ -1467,62 +1538,80 @@ export default function psyclawExtension(pi) {
1467
1538
  });
1468
1539
  if (!legacyTestApi)
1469
1540
  pi.registerCommand("ars", {
1470
- description: "进入 ARS 对话模式,或管理 doctor/start/full/stop",
1541
+ description: "切换 academic mode,或管理 doctor/start/full/stop",
1471
1542
  handler: async (args, ctx) => {
1472
1543
  try {
1473
1544
  const [action, ...rest] = args.trim().split(/\s+/).filter(Boolean);
1474
- // Bare /ars → conversation mode (ars: + tint). No profile popup.
1545
+ // Bare /ars → sticky academic mode (same as Shift+Tab on). No profile popup.
1475
1546
  if (!action) {
1476
1547
  if (!ctx.hasUI) {
1477
- ctx.ui.notify("当前环境无编辑器;请改用 /ars start 或发送 ars: <任务>。", "info");
1548
+ ctx.ui.notify("当前环境无编辑器;请改用 Shift+Tab 或 /ars start", "info");
1478
1549
  return;
1479
1550
  }
1480
- if (!isArsPiActive(ctx.sessionManager)) {
1481
- const approved = await ctx.ui.confirm("启用 ARS?", `来源:${ARS_REPOSITORY_URL} @ ${ARS_UPSTREAM_REF}\n许可:CC BY-NC 4.0,仅限非商业用途。关键研究阶段仍会请求确认。`);
1482
- if (!approved)
1483
- return;
1484
- }
1485
- ctx.ui.setEditorText(enterArsModeEditorText(ctx.ui.getEditorText?.() ?? ""));
1486
- ctx.ui.setStatus("ars", "ARS");
1551
+ arsUiContext = ctx;
1552
+ applySessionMode("academic");
1487
1553
  return;
1488
1554
  }
1489
1555
  if (action === "status") {
1490
1556
  ctx.ui.notify([
1491
1557
  `PsyClaw ARS profile v${PSYCLAW_ARS_PROFILE_VERSION}`,
1492
- "模式:轻量 ARS(输入 ars 后按 Tab,或以 ars: 前缀对话)",
1558
+ `当前模式:${arsModeEditor?.getMode() ?? sessionMode}(Shift+Tab:chat analysis academic)`,
1559
+ "Thinking:Ctrl+Shift+Tab",
1493
1560
  `来源:${ARS_REPOSITORY_URL} @ ${ARS_UPSTREAM_REF} (${ARS_UPSTREAM_COMMIT.slice(0, 12)})`,
1494
- "入口:ars: <task>,/ars doctor,/ars start,/ars full <task>,/ars stop",
1561
+ "入口:Shift+Tab,/ars doctor,/ars start,/ars full <task>,/ars stop,/verify",
1495
1562
  formatNatureArsFillerStatus(natureFillersFromCommandContext(ctx)),
1496
1563
  ].join("\n"), "info");
1497
1564
  return;
1498
1565
  }
1499
1566
  if (action === "install") {
1500
- ctx.ui.notify("ARS 与 Nature/compose 技能已内置:输入 ars 后按 Tab,或以 /ars start、/ars full <task> 使用。", "info");
1567
+ ctx.ui.notify("ARS 与 Nature/compose 技能已内置:Shift+Tab 切到 academic。", "info");
1501
1568
  return;
1502
1569
  }
1503
- if ((action === "start" || action === "full") && !isArsPiActive(ctx.sessionManager)) {
1504
- const approved = await ctx.ui.confirm("启用 ARS?", `来源:${ARS_REPOSITORY_URL} @ ${ARS_UPSTREAM_REF}\n许可:CC BY-NC 4.0,仅限非商业用途。关键研究阶段仍会请求确认。`);
1505
- if (!approved)
1506
- return;
1570
+ if (action === "doctor") {
1571
+ const activeTools = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
1572
+ const commands = typeof pi.getCommands === "function"
1573
+ ? pi.getCommands().map((command) => command.name)
1574
+ : [];
1575
+ const toolNames = [...new Set([
1576
+ ...activeTools.map((name) => String(name)),
1577
+ "psyclaw_ars_multi_agent",
1578
+ ])];
1579
+ const commandNames = [...new Set([...commands.map(String), "agents", "create-subagent"])];
1580
+ const report = await buildArsDoctorReport({
1581
+ repositoryRoot: arsRoot(),
1582
+ activeTools: toolNames,
1583
+ commands: commandNames,
1584
+ controlledRunActive: Boolean(await readControlledRun(ctx.cwd)),
1585
+ });
1586
+ if (typeof pi.sendMessage === "function") {
1587
+ pi.sendMessage({ customType: "psyclaw-ars-doctor", content: report, display: true });
1588
+ }
1589
+ else {
1590
+ ctx.ui.notify(report, "info");
1591
+ }
1592
+ return;
1507
1593
  }
1508
- const upstreamCommand = action === "doctor"
1509
- ? "/ars-pi-doctor"
1510
- : action === "start"
1511
- ? "/ars-pi-start"
1512
- : action === "stop"
1513
- ? "/ars-pi-stop"
1514
- : action === "full"
1515
- ? `/ars-full${rest.length > 0 ? ` ${rest.join(" ")}` : ""}`
1516
- : undefined;
1517
- if (!upstreamCommand) {
1518
- ctx.ui.notify("Usage: /ars [status|doctor|start|full <task>|stop] 或直接输入 ars 后按 Tab", "info");
1594
+ if (action === "start") {
1595
+ arsUiContext = ctx;
1596
+ applySessionMode("academic");
1597
+ ctx.ui.notify("academic mode 已开启(Shift+Tab 可切换)", "info");
1519
1598
  return;
1520
1599
  }
1521
1600
  if (action === "stop") {
1522
- ctx.ui.setStatus("ars", undefined);
1601
+ arsUiContext = ctx;
1602
+ applySessionMode("chat");
1523
1603
  if (ctx.hasUI && isArsModeEditorText(ctx.ui.getEditorText?.() ?? "")) {
1524
1604
  ctx.ui.setEditorText("");
1525
1605
  }
1606
+ ctx.ui.notify("已回到 chat 模式", "info");
1607
+ return;
1608
+ }
1609
+ const upstreamCommand = action === "full"
1610
+ ? `/ars-full${rest.length > 0 ? ` ${rest.join(" ")}` : ""}`
1611
+ : undefined;
1612
+ if (!upstreamCommand) {
1613
+ ctx.ui.notify("Usage: /ars [status|doctor|start|full <task>|stop] 或按 Shift+Tab 切换 academic mode", "info");
1614
+ return;
1526
1615
  }
1527
1616
  // A queued prompt does not get a fresh system prompt in Pi. Wait so the
1528
1617
  // upstream wrapper can activate ARS and inject its compatibility note.
@@ -1882,7 +1971,7 @@ export default function psyclawExtension(pi) {
1882
1971
  pi.registerTool({
1883
1972
  name: "psyclaw_ars_multi_agent",
1884
1973
  label: "ARS multi-agent review bridge",
1885
- description: "Run ARS reviewer_full Stage 3 as five process-separated read-only reviewer seats, or Stage 3 re-review as three ordered fenced calls. This is a parallel-agent capability; it preserves ARS checkpoints and never claims independent error processes.",
1974
+ description: "Run ARS reviewer_full Stage 3 as five process-separated read-only reviewer seats, or Stage 3 re-review as three ordered fenced calls. This is a parallel-agent / multi-agent / subagent workflow capability; it preserves ARS checkpoints and never claims independent error processes.",
1886
1975
  parameters: Type.Object({
1887
1976
  mode: Type.Union([Type.Literal("reviewer_full"), Type.Literal("reviewer_re_review")]),
1888
1977
  manuscriptPath: Type.Optional(Type.String()),
@@ -1921,6 +2010,21 @@ export default function psyclawExtension(pi) {
1921
2010
  }
1922
2011
  },
1923
2012
  });
2013
+ pi.registerTool({
2014
+ name: "psyclaw_ensure_pdf_engine",
2015
+ label: "Ensure PDF engine",
2016
+ description: "Install or locate a PDF engine (tectonic/xelatex) only when the user explicitly asked to export PDF. Do not call during /ars doctor or ordinary startup checks.",
2017
+ parameters: Type.Object({}),
2018
+ executionMode: "sequential",
2019
+ async execute() {
2020
+ const result = await ensurePdfEngineForExport();
2021
+ return {
2022
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2023
+ details: result,
2024
+ ...(result.status === "missing" || result.status === "unsupported-platform" ? { isError: true } : {}),
2025
+ };
2026
+ },
2027
+ });
1924
2028
  pi.registerTool({
1925
2029
  name: "psyclaw_create",
1926
2030
  label: "Create project capability",
@@ -2017,21 +2121,24 @@ export default function psyclawExtension(pi) {
2017
2121
  executionMode: "sequential",
2018
2122
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
2019
2123
  try {
2124
+ // Soft gate: /run is optional denser tooling, not a hard block on getting results.
2125
+ const controlled = await readControlledRun(ctx.cwd);
2126
+ const softNote = controlled
2127
+ ? undefined
2128
+ : "受控 /run 未启用(软门禁):继续执行;关键字段请 AI 核查并由人 /verify。";
2020
2129
  const request = params.request.trim();
2021
- if (!(await readControlledRun(ctx.cwd))) {
2022
- return {
2023
- content: [{ type: "text", text: JSON.stringify({
2024
- schemaVersion: "psyclaw/workbench-routing/v1",
2025
- status: "general-mode",
2026
- message: "PsyClaw controlled research mode is inactive. Continue as a normal conversational agent; do not impose research workflow gates. The user can run /init and then /run when they want the controlled workflow.",
2027
- }, null, 2) }],
2028
- details: { status: "general-mode", request },
2029
- };
2030
- }
2031
2130
  const combined = `${request} ${params.identifier ?? ""}`;
2032
2131
  const explicitWorkflow = params.workflow?.trim();
2033
2132
  if (explicitWorkflow) {
2034
- return runWorkflowTool(ctx.cwd, explicitWorkflow, params.target, params.nStudies);
2133
+ const result = await runWorkflowTool(ctx.cwd, explicitWorkflow, params.target, params.nStudies);
2134
+ if (softNote && result && typeof result === "object" && "content" in result) {
2135
+ const text = Array.isArray(result.content) ? result.content.map((part) => part.text ?? "").join("\n") : "";
2136
+ return {
2137
+ ...result,
2138
+ content: [{ type: "text", text: `${softNote}\n${text}` }],
2139
+ };
2140
+ }
2141
+ return result;
2035
2142
  }
2036
2143
  const metaAnalysisIntent = /meta[- ]?analysis|meta分析|元分析|系统综述|系统评价|效应量|forest plot|funnel|publication bias/i.test(combined);
2037
2144
  const publishIntent = /publish|finalize|发布|导出|定稿|投稿|export (to )?(docx|word|apa)|docx export|apa ?7/i.test(combined);