psyclaw 0.28.3 → 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 (38) hide show
  1. package/README.md +8 -5
  2. package/agents/recommended/catalog.json +1 -1
  3. package/dist/src/adapters/pi/extension.js +167 -80
  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/mode-editor.d.ts +14 -7
  11. package/dist/src/ars/mode-editor.js +39 -23
  12. package/dist/src/ars/mode-editor.js.map +1 -1
  13. package/dist/src/ars/profile.d.ts +10 -0
  14. package/dist/src/ars/profile.js +15 -2
  15. package/dist/src/ars/profile.js.map +1 -1
  16. package/dist/src/branding.js +12 -13
  17. package/dist/src/branding.js.map +1 -1
  18. package/dist/src/index.d.ts +4 -0
  19. package/dist/src/index.js +4 -0
  20. package/dist/src/index.js.map +1 -1
  21. package/dist/src/project/bootstrap.d.ts +7 -2
  22. package/dist/src/project/bootstrap.js +14 -13
  23. package/dist/src/project/bootstrap.js.map +1 -1
  24. package/dist/src/project/paths.d.ts +9 -0
  25. package/dist/src/project/paths.js +18 -3
  26. package/dist/src/project/paths.js.map +1 -1
  27. package/dist/src/project/workspace.d.ts +14 -0
  28. package/dist/src/project/workspace.js +82 -0
  29. package/dist/src/project/workspace.js.map +1 -0
  30. package/dist/src/session/modes.d.ts +10 -0
  31. package/dist/src/session/modes.js +52 -0
  32. package/dist/src/session/modes.js.map +1 -0
  33. package/dist/src/verify/checklist.d.ts +20 -0
  34. package/dist/src/verify/checklist.js +71 -0
  35. package/dist/src/verify/checklist.js.map +1 -0
  36. package/package.json +1 -1
  37. package/vendor/ars/pi/wrapper.js +14 -5
  38. 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.3`。正式命令、用户配置目录和后续发布统一使用 `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.3
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.3 --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.3 --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 | `Shift+Tab` 进入 academic mode(底栏显示);thinking level 改用 `Ctrl+Shift+Tab`;也可用 `/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.3",
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";
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";
24
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) {
@@ -1120,31 +1124,35 @@ export default function psyclawExtension(pi) {
1120
1124
  const runtimeMcps = new RuntimeMcpRegistry();
1121
1125
  let arsModeEditor;
1122
1126
  let arsUiContext;
1123
- const applyArsUiMode = (active, opts) => {
1124
- arsModeEditor?.setConversationMode(active);
1125
- arsUiContext?.ui.setStatus("ars", active ? ARS_MODE_STATUS : undefined);
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);
1126
1137
  if (opts?.syncSession === false || !arsUiContext)
1127
1138
  return;
1128
- const sessionActive = isArsPiActive(arsUiContext.sessionManager);
1129
- if (active && !sessionActive) {
1130
- pi.sendUserMessage("/ars-pi-start", {
1131
- ...(!arsUiContext.isIdle() ? { deliverAs: "followUp" } : {}),
1132
- expandPromptTemplates: true,
1133
- });
1134
- }
1135
- else if (!active && sessionActive) {
1136
- pi.sendUserMessage("/ars-pi-stop", {
1137
- ...(!arsUiContext.isIdle() ? { deliverAs: "followUp" } : {}),
1138
- expandPromptTemplates: true,
1139
- });
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 });
1140
1144
  }
1141
1145
  };
1146
+ const cycleSessionMode = () => {
1147
+ const next = nextSessionMode(arsModeEditor?.getMode() ?? sessionMode);
1148
+ applySessionMode(next);
1149
+ arsUiContext?.ui.setStatus("mode", MODE_STATUS[next] ?? "chat");
1150
+ };
1142
1151
  if (!legacyTestApi && typeof pi.registerShortcut === "function") {
1143
1152
  pi.registerShortcut("shift+tab", {
1144
- description: "Toggle academic mode",
1153
+ description: "Cycle chat → analysis → academic",
1145
1154
  handler: () => {
1146
- const next = !(arsModeEditor?.isConversationMode() ?? isArsPiActive(arsUiContext?.sessionManager));
1147
- applyArsUiMode(next);
1155
+ cycleSessionMode();
1148
1156
  },
1149
1157
  });
1150
1158
  }
@@ -1174,14 +1182,20 @@ export default function psyclawExtension(pi) {
1174
1182
  });
1175
1183
  if (!legacyTestApi && typeof pi.on === "function")
1176
1184
  pi.on("before_agent_start", (event, ctx) => {
1177
- if (!isArsPiTurn(event.systemPrompt) && !isArsPiActive(ctx?.sessionManager))
1178
- return;
1179
- return {
1180
- systemPrompt: `${event.systemPrompt}\n\n${psyclawArsPatch({
1181
- skills: event.systemPromptOptions?.skills,
1182
- systemPrompt: event.systemPrompt,
1183
- })}`,
1184
- };
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}` };
1185
1199
  });
1186
1200
  if (!legacyTestApi && typeof pi.on === "function")
1187
1201
  pi.on("session_start", (_event, ctx) => {
@@ -1191,15 +1205,24 @@ export default function psyclawExtension(pi) {
1191
1205
  ctx.ui.setEditorComponent((tui, theme, keybindings) => {
1192
1206
  const editor = new ArsModeEditor(tui, theme, keybindings);
1193
1207
  arsModeEditor = editor;
1194
- editor.onArsModeChange = (active) => {
1195
- ctx.ui.setStatus("ars", active ? ARS_MODE_STATUS : undefined);
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);
1196
1215
  };
1197
- editor.onToggleConversationMode = () => {
1198
- applyArsUiMode(!editor.isConversationMode());
1216
+ editor.onCycleMode = () => {
1217
+ cycleSessionMode();
1199
1218
  };
1200
- const restore = isArsPiActive(ctx.sessionManager);
1201
- editor.setConversationMode(restore);
1202
- if (restore)
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")
1203
1226
  ctx.ui.setStatus("ars", ARS_MODE_STATUS);
1204
1227
  return editor;
1205
1228
  });
@@ -1247,18 +1270,42 @@ export default function psyclawExtension(pi) {
1247
1270
  return undefined;
1248
1271
  });
1249
1272
  pi.registerCommand("init", {
1250
- description: "初始化可追溯的研究项目",
1273
+ description: "搭建干净工作仓库(目录 + psyclaw.md),不自动追问或启动 /run",
1251
1274
  handler: async (args, ctx) => {
1252
1275
  try {
1253
1276
  const parsed = parseInitArgs(args);
1254
1277
  const project = await bootstrapProject({ root: ctx.cwd, ...parsed });
1255
- if (typeof pi.sendUserMessage === "function") {
1256
- pi.sendUserMessage(academicGrillRequest(project.goal, "init"), ctx.isIdle() ? {} : { deliverAs: "followUp" });
1257
- 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;
1258
1299
  }
1259
- else {
1260
- 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;
1261
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");
1262
1309
  }
1263
1310
  catch (error) {
1264
1311
  await notifyError(ctx, error);
@@ -1276,7 +1323,7 @@ export default function psyclawExtension(pi) {
1276
1323
  project = await readProject(ctx.cwd);
1277
1324
  }
1278
1325
  catch {
1279
- ctx.ui.notify("当前目录还没有通过 /init 建立的研究记录,因此保持普通对话模式。需要受控研究流程时,请先运行 /init,再运行 /run。", "warning");
1326
+ ctx.ui.notify("当前目录还没有通过 /init 建立的研究记录。可先 /init 建仓;需要更密受控工具链时再 /run。", "warning");
1280
1327
  return;
1281
1328
  }
1282
1329
  const available = await selectableRunSkills(ctx.cwd);
@@ -1303,9 +1350,9 @@ export default function psyclawExtension(pi) {
1303
1350
  }
1304
1351
  },
1305
1352
  });
1306
- if (developerCommands || legacyTestApi)
1307
- pi.registerCommand("verify", {
1308
- description: "写入机器可读的交接检查点",
1353
+ if (developerCommands)
1354
+ pi.registerCommand("handoff", {
1355
+ description: "(开发)写入机器可读交接检查点",
1309
1356
  handler: async (_args, ctx) => {
1310
1357
  try {
1311
1358
  const paths = projectPaths(ctx.cwd);
@@ -1321,7 +1368,7 @@ export default function psyclawExtension(pi) {
1321
1368
  verificationCommands: ["pnpm typecheck", "pnpm test"],
1322
1369
  generatedAt: new Date().toISOString(),
1323
1370
  });
1324
- 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");
1325
1372
  }
1326
1373
  catch (error) {
1327
1374
  await notifyError(ctx, error);
@@ -1502,47 +1549,69 @@ export default function psyclawExtension(pi) {
1502
1549
  return;
1503
1550
  }
1504
1551
  arsUiContext = ctx;
1505
- applyArsUiMode(true);
1552
+ applySessionMode("academic");
1506
1553
  return;
1507
1554
  }
1508
1555
  if (action === "status") {
1509
1556
  ctx.ui.notify([
1510
1557
  `PsyClaw ARS profile v${PSYCLAW_ARS_PROFILE_VERSION}`,
1511
- "模式:academic mode(Shift+Tab 切换;开启后直接对话)",
1512
- "Thinking:Ctrl+Shift+Tab(Shift+Tab 已让给 academic mode)",
1558
+ `当前模式:${arsModeEditor?.getMode() ?? sessionMode}(Shift+Tab:chat → analysis → academic)`,
1559
+ "Thinking:Ctrl+Shift+Tab",
1513
1560
  `来源:${ARS_REPOSITORY_URL} @ ${ARS_UPSTREAM_REF} (${ARS_UPSTREAM_COMMIT.slice(0, 12)})`,
1514
- "入口:Shift+Tab,/ars doctor,/ars start,/ars full <task>,/ars stop",
1561
+ "入口:Shift+Tab,/ars doctor,/ars start,/ars full <task>,/ars stop,/verify",
1515
1562
  formatNatureArsFillerStatus(natureFillersFromCommandContext(ctx)),
1516
1563
  ].join("\n"), "info");
1517
1564
  return;
1518
1565
  }
1519
1566
  if (action === "install") {
1520
- ctx.ui.notify("ARS 与 Nature/compose 技能已内置:按 Shift+Tab 进入 academic mode。", "info");
1567
+ ctx.ui.notify("ARS 与 Nature/compose 技能已内置:Shift+Tab 切到 academic。", "info");
1521
1568
  return;
1522
1569
  }
1523
- const upstreamCommand = action === "doctor"
1524
- ? "/ars-pi-doctor"
1525
- : action === "start"
1526
- ? "/ars-pi-start"
1527
- : action === "stop"
1528
- ? "/ars-pi-stop"
1529
- : action === "full"
1530
- ? `/ars-full${rest.length > 0 ? ` ${rest.join(" ")}` : ""}`
1531
- : undefined;
1532
- if (!upstreamCommand) {
1533
- ctx.ui.notify("Usage: /ars [status|doctor|start|full <task>|stop] 或按 Shift+Tab 切换 academic mode", "info");
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
+ }
1534
1592
  return;
1535
1593
  }
1536
1594
  if (action === "start") {
1537
1595
  arsUiContext = ctx;
1538
- applyArsUiMode(true, { syncSession: false });
1596
+ applySessionMode("academic");
1597
+ ctx.ui.notify("academic mode 已开启(Shift+Tab 可切换)", "info");
1598
+ return;
1539
1599
  }
1540
1600
  if (action === "stop") {
1541
1601
  arsUiContext = ctx;
1542
- applyArsUiMode(false, { syncSession: false });
1602
+ applySessionMode("chat");
1543
1603
  if (ctx.hasUI && isArsModeEditorText(ctx.ui.getEditorText?.() ?? "")) {
1544
1604
  ctx.ui.setEditorText("");
1545
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;
1546
1615
  }
1547
1616
  // A queued prompt does not get a fresh system prompt in Pi. Wait so the
1548
1617
  // upstream wrapper can activate ARS and inject its compatibility note.
@@ -1902,7 +1971,7 @@ export default function psyclawExtension(pi) {
1902
1971
  pi.registerTool({
1903
1972
  name: "psyclaw_ars_multi_agent",
1904
1973
  label: "ARS multi-agent review bridge",
1905
- 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.",
1906
1975
  parameters: Type.Object({
1907
1976
  mode: Type.Union([Type.Literal("reviewer_full"), Type.Literal("reviewer_re_review")]),
1908
1977
  manuscriptPath: Type.Optional(Type.String()),
@@ -1941,6 +2010,21 @@ export default function psyclawExtension(pi) {
1941
2010
  }
1942
2011
  },
1943
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
+ });
1944
2028
  pi.registerTool({
1945
2029
  name: "psyclaw_create",
1946
2030
  label: "Create project capability",
@@ -2037,21 +2121,24 @@ export default function psyclawExtension(pi) {
2037
2121
  executionMode: "sequential",
2038
2122
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
2039
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。";
2040
2129
  const request = params.request.trim();
2041
- if (!(await readControlledRun(ctx.cwd))) {
2042
- return {
2043
- content: [{ type: "text", text: JSON.stringify({
2044
- schemaVersion: "psyclaw/workbench-routing/v1",
2045
- status: "general-mode",
2046
- 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.",
2047
- }, null, 2) }],
2048
- details: { status: "general-mode", request },
2049
- };
2050
- }
2051
2130
  const combined = `${request} ${params.identifier ?? ""}`;
2052
2131
  const explicitWorkflow = params.workflow?.trim();
2053
2132
  if (explicitWorkflow) {
2054
- 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;
2055
2142
  }
2056
2143
  const metaAnalysisIntent = /meta[- ]?analysis|meta分析|元分析|系统综述|系统评价|效应量|forest plot|funnel|publication bias/i.test(combined);
2057
2144
  const publishIntent = /publish|finalize|发布|导出|定稿|投稿|export (to )?(docx|word|apa)|docx export|apa ?7/i.test(combined);