psyclaw 0.28.3 → 0.29.2

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 (66) hide show
  1. package/README.md +12 -11
  2. package/agents/recommended/catalog.json +2 -2
  3. package/dist/src/adapters/pi/extension.js +310 -256
  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/analysis/plan.d.ts +82 -0
  8. package/dist/src/analysis/plan.js +246 -0
  9. package/dist/src/analysis/plan.js.map +1 -0
  10. package/dist/src/analysis/stats-router.d.ts +13 -0
  11. package/dist/src/analysis/stats-router.js +44 -0
  12. package/dist/src/analysis/stats-router.js.map +1 -0
  13. package/dist/src/ars/academic-router.d.ts +33 -0
  14. package/dist/src/ars/academic-router.js +204 -0
  15. package/dist/src/ars/academic-router.js.map +1 -0
  16. package/dist/src/ars/doctor.d.ts +27 -0
  17. package/dist/src/ars/doctor.js +121 -0
  18. package/dist/src/ars/doctor.js.map +1 -0
  19. package/dist/src/ars/mode-editor.d.ts +14 -7
  20. package/dist/src/ars/mode-editor.js +39 -23
  21. package/dist/src/ars/mode-editor.js.map +1 -1
  22. package/dist/src/ars/profile.d.ts +10 -0
  23. package/dist/src/ars/profile.js +15 -2
  24. package/dist/src/ars/profile.js.map +1 -1
  25. package/dist/src/branding.js +15 -16
  26. package/dist/src/branding.js.map +1 -1
  27. package/dist/src/cli.js +1 -8
  28. package/dist/src/cli.js.map +1 -1
  29. package/dist/src/index.d.ts +6 -1
  30. package/dist/src/index.js +6 -1
  31. package/dist/src/index.js.map +1 -1
  32. package/dist/src/panel/server.js +1 -1
  33. package/dist/src/panel/server.js.map +1 -1
  34. package/dist/src/project/approvals.d.ts +4 -3
  35. package/dist/src/project/approvals.js +4 -3
  36. package/dist/src/project/approvals.js.map +1 -1
  37. package/dist/src/project/bootstrap.d.ts +7 -2
  38. package/dist/src/project/bootstrap.js +14 -13
  39. package/dist/src/project/bootstrap.js.map +1 -1
  40. package/dist/src/project/paths.d.ts +10 -0
  41. package/dist/src/project/paths.js +20 -3
  42. package/dist/src/project/paths.js.map +1 -1
  43. package/dist/src/project/workspace.d.ts +14 -0
  44. package/dist/src/project/workspace.js +82 -0
  45. package/dist/src/project/workspace.js.map +1 -0
  46. package/dist/src/session/modes.d.ts +10 -0
  47. package/dist/src/session/modes.js +57 -0
  48. package/dist/src/session/modes.js.map +1 -0
  49. package/dist/src/skills/recommended.js +1 -1
  50. package/dist/src/skills/recommended.js.map +1 -1
  51. package/dist/src/style/cli-ui.js +3 -3
  52. package/dist/src/style/cli-ui.js.map +1 -1
  53. package/dist/src/verify/checklist.d.ts +20 -0
  54. package/dist/src/verify/checklist.js +71 -0
  55. package/dist/src/verify/checklist.js.map +1 -0
  56. package/package.json +2 -1
  57. package/scripts/rebrand-pi.mjs +2 -2
  58. package/skills/core/academic-grill/SKILL.md +1 -1
  59. package/skills/core/analysis-plan/SKILL.md +29 -0
  60. package/skills/core/manifest.json +3 -3
  61. package/vendor/ars/pi/wrapper.js +14 -5
  62. package/vendor/ars/pi/wrapper.test.mjs +10 -0
  63. package/dist/src/research/brief.d.ts +0 -11
  64. package/dist/src/research/brief.js +0 -81
  65. package/dist/src/research/brief.js.map +0 -1
  66. package/skills/core/research-brief/SKILL.md +0 -17
@@ -1,6 +1,6 @@
1
1
  import { DefaultPackageManager, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
- import { appendApproval, approvalInputDigest, asProject, assertResearchDecision, bootstrapProject, projectPaths, readApprovals, resolveResearchDecision, runOfflineBrief, runInstitutionalFulltext, runLiteratureReview, runExpertReview, runAnalysisDelegation, runWritingReview, runMetaAnalysis, createStageRunner, exportAcademicDocument, recordCitationUse, runParallelPeerReview, writeHandoff } from "../../index.js";
3
+ import { appendApproval, approvalInputDigest, asProject, assertResearchDecision, bootstrapProject, projectPaths, resolveResearchDecision, runInstitutionalFulltext, runLiteratureReview, runExpertReview, runAnalysisDelegation, runWritingReview, runMetaAnalysis, createStageRunner, exportAcademicDocument, recordCitationUse, runParallelPeerReview, writeHandoff } from "../../index.js";
4
4
  import { runPlanWithPi } from "../../orchestration/pi-executor.js";
5
5
  import { customPersonaPlan, parseAgentsRequest } from "../../orchestration/personas.js";
6
6
  import { elevatedEffects, formatEffects, hasElevatedEffects, normalizeEffects, toolsForEffects } from "../../orchestration/effects.js";
@@ -20,8 +20,15 @@ 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 { formatAcademicSoftRouteInvocation, rankAcademicSoftRoutes, resolveAcademicSoftRoute, } from "../../ars/academic-router.js";
26
+ import { advanceAnalysisPlan, createAnalysisPlan, formatAnalysisPlanStatus, listAnalysisPlans, readActiveAnalysisPlan, syncHandoffFromAnalysisPlan, writeAnalysisPlan, } from "../../analysis/plan.js";
27
+ import { formatAnalysisPlanTakeover, resolveStatsIntent, } from "../../analysis/stats-router.js";
28
+ import { arsRoot } from "../../ars/pi-panel-executor.js";
24
29
  import { ArsModeEditor, ARS_MODE_STATUS, isArsModeEditorText } from "../../ars/mode-editor.js";
30
+ import { MODE_STATUS, nextSessionMode, parseSessionMode, sessionModePrompt, } from "../../session/modes.js";
31
+ import { formatVerifyChecklist, loadVerifyChecklist, markVerifyItem } from "../../verify/checklist.js";
25
32
  const PARADIGMS = new Set([
26
33
  "survey-observational",
27
34
  "qualitative-thematic",
@@ -36,19 +43,19 @@ const PARADIGMS = new Set([
36
43
  ]);
37
44
  function parseInitArgs(args) {
38
45
  const trimmed = args.trim();
46
+ if (!trimmed)
47
+ return {};
39
48
  const match = trimmed.match(/^--paradigm(?:=|\s+)(\S+)(?:\s+([\s\S]*))?$/i);
40
49
  if (trimmed.startsWith("--")) {
41
50
  const paradigm = match?.[1];
42
51
  const goal = match?.[2]?.trim();
43
- if (!match || !paradigm || !goal) {
44
- throw new Error("Usage: /init [--paradigm survey-observational] <research goal>");
52
+ if (!match || !paradigm) {
53
+ throw new Error("Usage: /init [--paradigm survey-observational] [optional goal]");
45
54
  }
46
55
  if (!PARADIGMS.has(paradigm))
47
56
  throw new Error(`Unsupported paradigm: ${paradigm}`);
48
- return { paradigm, goal };
57
+ return { paradigm, ...(goal ? { goal } : {}) };
49
58
  }
50
- if (!trimmed)
51
- throw new Error("Usage: /init [--paradigm survey-observational] <research goal>");
52
59
  return { paradigm: "survey-observational", goal: trimmed };
53
60
  }
54
61
  async function notifyError(ctx, error) {
@@ -63,7 +70,7 @@ async function runPluginCommand(args) {
63
70
  throw new Error(`不支持的 Plugin 操作:${args[0] ?? ""}`);
64
71
  }
65
72
  const activeAgentRuns = new Set();
66
- const CORE_SKILLS = new Set(["academic-grill", "research-intake", "evidence-capture", "citation-audit", "research-brief"]);
73
+ const CORE_SKILLS = new Set(["academic-grill", "research-intake", "evidence-capture", "citation-audit", "analysis-plan"]);
67
74
  function findLastMatching(items, predicate) {
68
75
  for (let index = items.length - 1; index >= 0; index -= 1) {
69
76
  const item = items[index];
@@ -196,80 +203,19 @@ function academicGrillRequest(subject, mode) {
196
203
  : "持久化模式:review。追问完成后先展示拟更新摘要和受影响文件,并询问我是否写回;得到明确确认前不得修改项目文档。",
197
204
  ].join("\n");
198
205
  }
199
- async function controlledRunPath(root) {
200
- return join(projectPaths(root).root, ".psyclaw", "controlled-run.json");
201
- }
202
- async function readControlledRun(root) {
206
+ async function readActiveProject(root) {
203
207
  try {
204
- const value = JSON.parse(await readFile(await controlledRunPath(root), "utf8"));
205
- if (!(value.schemaVersion === "psyclaw/controlled-run/v1" && value.status === "active" && typeof value.runId === "string" && typeof value.projectId === "string" && typeof value.objective === "string" && typeof value.activatedAt === "string"))
206
- return null;
207
208
  const project = await readProject(root);
208
- if (project.id !== value.projectId)
209
- return null;
210
- const authorization = findLastMatching(await readApprovals(root), (record) => record.kind === "run-mode" && record.runId === value.runId);
211
- if (authorization?.decision !== "approved" || authorization.actor !== "human")
212
- return null;
213
- return { ...value, mode: value.mode === "auto" ? "auto" : "human", selectedSkills: Array.isArray(value.selectedSkills) ? value.selectedSkills.filter((item) => typeof item === "string") : [] };
209
+ return {
210
+ projectId: project.id,
211
+ runId: `project_${project.id}`,
212
+ objective: project.goal,
213
+ };
214
214
  }
215
215
  catch {
216
216
  return null;
217
217
  }
218
218
  }
219
- async function activateControlledRun(root, runId, projectId, objective, selectedSkills, mode) {
220
- const state = {
221
- schemaVersion: "psyclaw/controlled-run/v1",
222
- runId,
223
- projectId,
224
- objective,
225
- selectedSkills: [...new Set(selectedSkills)],
226
- mode,
227
- activatedAt: new Date().toISOString(),
228
- status: "active",
229
- };
230
- await atomicWriteFile(await controlledRunPath(root), `${JSON.stringify(state, null, 2)}\n`);
231
- return state;
232
- }
233
- function controlledRunRequest(objective, selectedSkills, mode) {
234
- return [
235
- `PsyClaw 受控研究流程已由用户通过 /run 明确启动${mode === "auto" ? ",本次采用连续执行方式" : ""}。先读取 .psyclaw/project.json、.psyclaw/controlled-run.json、.psyclaw/approvals.jsonl、notes/research-spec.md 和 notes/plan.md。`,
236
- `本次目标:${objective}`,
237
- selectedSkills.length > 0
238
- ? `本次运行由用户选择的优化 Skill:${selectedSkills.join(", ")}。在相关任务中优先加载并遵循这些 Skill;同名 Skill 只加载一次。`
239
- : "本次运行未指定额外 Skill,使用 PsyClaw 默认研究流程。不要删除、停用或改写已安装的其他 Skill。",
240
- "计划内的文件创建、脚本执行、分析委托、可恢复下载和普通工具调用连续推进,不要逐项向用户索要许可;/run 已代表用户同意这些为完成本次目标所必需且符合项目边界的操作,系统仍须记录结构化回执。凭据处理、原始数据覆盖、破坏性命令、绕过访问控制和外部发布不在该授权范围内。",
241
- "人工决策只用于真正的研究取舍:存在两个以上有实质依据的合理方案,现有研究问题、数据、证据和方法规范不能确定其中之一,且不同选择会改变研究问题、样本纳入或排除、变量操作化、估计目标、分析方法或结果解释。提出决策前必须先核对可用证据与通行方法;提出时说明仍无法消解的原因、备选方案、各自依据与影响,并给出系统推荐;一次只请求一个决定。",
242
- "满足上述全部条件时,先调用 psyclaw_research_decision 写入结构化研究取舍事件,再向研究者提问。该工具拒绝的事项不得改用自然语言伪装成 awaiting-human;应继续自动修复、明确报告技术限制,或按独立安全授权边界处理。",
243
- "不要因软件缺失、安装配置、格式问题、脚本错误、字段可推断、统计报告遗漏、效应量或置信区间缺失、缺失值说明不足、引用格式错误或可补充的证据不足而进入人工决策。先自行安装或修复、重跑、补算、补写、检索核验或缩小表述;确实无法完成时报告具体未完成项和下一步,不把技术失败包装成研究者取舍。",
244
- "实际推进当前尚未完成的研究任务;先形成或更新符合学术规范的 Markdown 分析报告,不能只描述计划。分析报告必须清楚区分数据来源、统计结果、文献证据、限制与尚未核验内容。",
245
- "写作时把内部状态和校验术语留在 .psyclaw 记录中,不要把 Claim、Evidence、ledger、gate、audit、blocked、receipt 等标签直接写进面向研究者的对话、摘要或论文。中文正文避免生硬使用“审计”“结局”“依赖”“受阻”等词:分别按语境改为质量检查、结果变量或指标、所需软件或前提、无法分析。",
246
- "摘要首先服从用户提供的会议或期刊格式;没有明确要求时写成一段式摘要,完整交代目的、方法、主要结果和结论,但不机械添加四个小标题。摘要只保留最关键结果,不罗列损坏列、逐列缺失数、文件哈希或实现细节。数据质量细节放在方法、附录或单独质量报告中。",
247
- "若用户只提供数据集,先结合变量、研究背景和可识别的理论机制主动提出 2-4 个有研究价值且可检验的问题或假设,再推荐分析主线。不要把“是否有预注册”作为第一个或默认问题;探索性分析明确标注即可,只有用户声明为确证性研究时才核对事前分析方案,且不索要公开链接。",
248
- "数据与隐私默认最小披露:对话、报告和论文先给汇总缺失率、受影响的变量组及其分析影响,不输出原始行、个体文本、标识符、完整列清单或冗长的逐列缺失信息;确需列名时只列与当前方法决策直接相关者,完整诊断另存补充材料。",
249
- "参考文献按目标格式生成并逐项核对。APA 7 稿件必须做到正文引文与文末条目一一对应,区分期刊论文、会议论文、数据集、预印本和软件,核对作者顺序、年份、标题、刊名或会议名、卷期页码或文章号、DOI、标点、排序及悬挂缩进;缺少字段时不得猜测。",
250
- "分析报告完成后,下一步不是直接询问是否导出 DOCX,而是询问用户是否据此撰写论文。",
251
- "若用户选择撰写论文,再单独询问是否先进行文献调研,并说明该阶段可能花费较长时间;提示用户可以指定已启用的 Skill,或者选择 PsyClaw 默认的文献调研方式。不要在获得答复前自动开始长时调研。",
252
- "文献调研完成并核验后,再询问是否进行全文撰写,并询问使用哪个写作 Skill;未指定时可以建议默认方案,但仍需用户确认。只有充分证据经过核验后,才能把相关主张写入论文正文。",
253
- "同行评审由用户需要时另行运行 /review;不要在本轮自动审稿。/review 会再次询问使用哪个评审 Skill。最后才询问是否导出 DOCX,以及格式要求。",
254
- "开放获取 PDF 自动保存到 literature/pdfs/。不得绕过付费墙;没有合法开放全文时给出 https://doi.org/<DOI> 可点击链接,提示用户通过自己的权限下载到系统给出的目标路径。论文中的每篇引用都必须完成 DOI 核验并有本地 PDF,否则用自然语言列出待补材料,并说明补齐前不宜定稿;不要向用户输出 blocked、gate 等内部状态标签。",
255
- "如果用户在未完成文献调研、全文写作或评审前要求导出 DOCX,允许导出当前分析报告,但必须明确标为遵循学术规范的分析报告,不得称为论文。",
256
- ].join("\n");
257
- }
258
- function parseRunArgs(args) {
259
- const value = args.trim();
260
- const auto = value === "auto" || value.startsWith("auto ");
261
- const rest = auto ? value.slice(4).trim() : value;
262
- if (!rest.startsWith("--skills"))
263
- return { objective: rest, mode: auto ? "auto" : "human" };
264
- const match = rest.match(/^--skills(?:=|\s+)([^\s]+)(?:\s+([\s\S]*))?$/);
265
- if (!match)
266
- throw new Error("Usage: /run [auto] [--skills skill-a,skill-b] [objective]");
267
- return {
268
- objective: (match[2] ?? "").trim(),
269
- requestedSkills: [...new Set(match[1].split(",").map((item) => item.trim()).filter(Boolean))],
270
- mode: auto ? "auto" : "human",
271
- };
272
- }
273
219
  function contextProjectTrusted(ctx) {
274
220
  const candidate = ctx;
275
221
  return typeof candidate.isProjectTrusted === "function" && candidate.isProjectTrusted();
@@ -290,41 +236,6 @@ function requiresSeparateOperationConfirmation(toolName, input) {
290
236
  const text = `${toolName} ${JSON.stringify(input ?? {})}`;
291
237
  return /submit\s+(?:manuscript|paper|form)|投稿(?:到|至)|发布到(?:外部|期刊|会议|网站)|send\s+(?:email|message)|邮件发送/i.test(text);
292
238
  }
293
- async function selectableRunSkills(root) {
294
- const state = await readRecommendationState(root);
295
- const rows = await skillManagerRows(root, state);
296
- const available = [
297
- ...coreSkillNames().map((name) => ({ id: name, name })),
298
- ...rows.filter((row) => row.installed && row.enabled && !row.blocked).map((row) => ({ id: row.id, name: row.name })),
299
- ];
300
- const seen = new Set();
301
- return available.filter((skill) => {
302
- const key = skill.name.trim().toLocaleLowerCase();
303
- if (!key || seen.has(key))
304
- return false;
305
- seen.add(key);
306
- return true;
307
- });
308
- }
309
- function resolveRunSkills(requested, available) {
310
- const lookup = new Map();
311
- for (const skill of available) {
312
- lookup.set(skill.id.toLocaleLowerCase(), skill.name);
313
- lookup.set(skill.name.toLocaleLowerCase(), skill.name);
314
- }
315
- const selected = [];
316
- const unknown = [];
317
- for (const value of requested) {
318
- const name = lookup.get(value.toLocaleLowerCase());
319
- if (!name)
320
- unknown.push(value);
321
- else if (!selected.some((item) => item.toLocaleLowerCase() === name.toLocaleLowerCase()))
322
- selected.push(name);
323
- }
324
- if (unknown.length > 0)
325
- throw new Error(`本次运行不可用的 Skill:${unknown.join(", ")}。请先通过 /skill 安装或启用。`);
326
- return selected;
327
- }
328
239
  async function recommendedItems(kind) {
329
240
  const file = kind === "skills" ? "catalog.json" : "mcp-catalog.json";
330
241
  const moduleDir = dirname(fileURLToPath(import.meta.url));
@@ -1120,31 +1031,35 @@ export default function psyclawExtension(pi) {
1120
1031
  const runtimeMcps = new RuntimeMcpRegistry();
1121
1032
  let arsModeEditor;
1122
1033
  let arsUiContext;
1123
- const applyArsUiMode = (active, opts) => {
1124
- arsModeEditor?.setConversationMode(active);
1125
- arsUiContext?.ui.setStatus("ars", active ? ARS_MODE_STATUS : undefined);
1034
+ let sessionMode = "chat";
1035
+ const applySessionMode = (mode, opts) => {
1036
+ sessionMode = mode;
1037
+ arsModeEditor?.setMode(mode);
1038
+ arsUiContext?.ui.setStatus("mode", MODE_STATUS[mode]);
1039
+ // Clear legacy ars status key when leaving academic.
1040
+ if (mode !== "academic")
1041
+ arsUiContext?.ui.setStatus("ars", undefined);
1042
+ else
1043
+ arsUiContext?.ui.setStatus("ars", ARS_MODE_STATUS);
1126
1044
  if (opts?.syncSession === false || !arsUiContext)
1127
1045
  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
- });
1046
+ setArsPiSessionActive(typeof pi.appendEntry === "function"
1047
+ ? pi.appendEntry.bind(pi)
1048
+ : undefined, mode === "academic");
1049
+ if (typeof pi.appendEntry === "function") {
1050
+ pi.appendEntry("psyclaw-session-mode", { mode });
1140
1051
  }
1141
1052
  };
1053
+ const cycleSessionMode = () => {
1054
+ const next = nextSessionMode(arsModeEditor?.getMode() ?? sessionMode);
1055
+ applySessionMode(next);
1056
+ arsUiContext?.ui.setStatus("mode", MODE_STATUS[next] ?? "chat");
1057
+ };
1142
1058
  if (!legacyTestApi && typeof pi.registerShortcut === "function") {
1143
1059
  pi.registerShortcut("shift+tab", {
1144
- description: "Toggle academic mode",
1060
+ description: "Cycle chat → analysis → academic",
1145
1061
  handler: () => {
1146
- const next = !(arsModeEditor?.isConversationMode() ?? isArsPiActive(arsUiContext?.sessionManager));
1147
- applyArsUiMode(next);
1062
+ cycleSessionMode();
1148
1063
  },
1149
1064
  });
1150
1065
  }
@@ -1166,7 +1081,7 @@ export default function psyclawExtension(pi) {
1166
1081
  pi.on("session_shutdown", () => runtimeMcps.close());
1167
1082
  if (!legacyTestApi && typeof pi.on === "function")
1168
1083
  pi.on("before_agent_start", async (event, ctx) => {
1169
- if (!(await readControlledRun(ctx?.cwd ?? process.cwd())))
1084
+ if (!(await readActiveProject(ctx?.cwd ?? process.cwd())))
1170
1085
  return;
1171
1086
  const prompt = userRulesPrompt(await loadUserRules(ctx?.cwd ?? process.cwd()));
1172
1087
  if (prompt)
@@ -1174,14 +1089,20 @@ export default function psyclawExtension(pi) {
1174
1089
  });
1175
1090
  if (!legacyTestApi && typeof pi.on === "function")
1176
1091
  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
- };
1092
+ const modeEntry = [...(ctx?.sessionManager?.getBranch?.() ?? [])].reverse().find((entry) => entry.type === "custom" && entry.customType === "psyclaw-session-mode");
1093
+ const restored = parseSessionMode(modeEntry?.data?.mode)
1094
+ ?? (isArsPiActive(ctx?.sessionManager) ? "academic" : sessionMode);
1095
+ sessionMode = restored;
1096
+ const modeBlock = sessionModePrompt(restored);
1097
+ if (restored === "academic" || isArsPiTurn(event.systemPrompt) || isArsPiActive(ctx?.sessionManager)) {
1098
+ return {
1099
+ systemPrompt: `${event.systemPrompt}\n\n${modeBlock}\n\n${psyclawArsPatch({
1100
+ skills: event.systemPromptOptions?.skills,
1101
+ systemPrompt: event.systemPrompt,
1102
+ })}`,
1103
+ };
1104
+ }
1105
+ return { systemPrompt: `${event.systemPrompt}\n\n${modeBlock}` };
1185
1106
  });
1186
1107
  if (!legacyTestApi && typeof pi.on === "function")
1187
1108
  pi.on("session_start", (_event, ctx) => {
@@ -1191,15 +1112,24 @@ export default function psyclawExtension(pi) {
1191
1112
  ctx.ui.setEditorComponent((tui, theme, keybindings) => {
1192
1113
  const editor = new ArsModeEditor(tui, theme, keybindings);
1193
1114
  arsModeEditor = editor;
1194
- editor.onArsModeChange = (active) => {
1195
- ctx.ui.setStatus("ars", active ? ARS_MODE_STATUS : undefined);
1115
+ editor.onModeChange = (mode) => {
1116
+ sessionMode = mode;
1117
+ ctx.ui.setStatus("mode", MODE_STATUS[mode]);
1118
+ if (mode === "academic")
1119
+ ctx.ui.setStatus("ars", ARS_MODE_STATUS);
1120
+ else
1121
+ ctx.ui.setStatus("ars", undefined);
1196
1122
  };
1197
- editor.onToggleConversationMode = () => {
1198
- applyArsUiMode(!editor.isConversationMode());
1123
+ editor.onCycleMode = () => {
1124
+ cycleSessionMode();
1199
1125
  };
1200
- const restore = isArsPiActive(ctx.sessionManager);
1201
- editor.setConversationMode(restore);
1202
- if (restore)
1126
+ const modeEntry = [...ctx.sessionManager.getBranch()].reverse().find((entry) => entry.type === "custom" && entry.customType === "psyclaw-session-mode");
1127
+ const restore = parseSessionMode(modeEntry?.data?.mode)
1128
+ ?? (isArsPiActive(ctx.sessionManager) ? "academic" : "chat");
1129
+ editor.setMode(restore);
1130
+ sessionMode = restore;
1131
+ ctx.ui.setStatus("mode", MODE_STATUS[restore]);
1132
+ if (restore === "academic")
1203
1133
  ctx.ui.setStatus("ars", ARS_MODE_STATUS);
1204
1134
  return editor;
1205
1135
  });
@@ -1208,14 +1138,34 @@ export default function psyclawExtension(pi) {
1208
1138
  pi.on("input", (event, ctx) => {
1209
1139
  if (event.source === "extension")
1210
1140
  return;
1211
- if (!/^\/(?:login|logout)(?:\s|$)/i.test(event.text.trim()))
1212
- return;
1213
- ctx.ui.notify("PsyClaw 已统一隐藏底层登录命令。请使用 /provider 配置、切换或更新模型凭据。", "info");
1214
- return { action: "handled" };
1141
+ if (/^\/(?:login|logout)(?:\s|$)/i.test(event.text.trim())) {
1142
+ ctx.ui.notify("PsyClaw 已统一隐藏底层登录命令。请使用 /provider 配置、切换或更新模型凭据。", "info");
1143
+ return { action: "handled" };
1144
+ }
1145
+ const mode = arsModeEditor?.getMode() ?? sessionMode;
1146
+ if (mode === "academic") {
1147
+ const match = resolveAcademicSoftRoute(event.text);
1148
+ if (!match)
1149
+ return;
1150
+ const ranked = rankAcademicSoftRoutes(event.text, 3);
1151
+ return {
1152
+ action: "transform",
1153
+ text: formatAcademicSoftRouteInvocation(match, event.text, ranked.slice(1)),
1154
+ };
1155
+ }
1156
+ if (mode === "analysis") {
1157
+ const match = resolveStatsIntent(event.text);
1158
+ if (!match)
1159
+ return;
1160
+ return {
1161
+ action: "transform",
1162
+ text: formatAnalysisPlanTakeover(event.text),
1163
+ };
1164
+ }
1215
1165
  });
1216
1166
  if (!legacyTestApi && typeof pi.on === "function")
1217
1167
  pi.on("tool_call", async (event, ctx) => {
1218
- const run = await readControlledRun(ctx.cwd);
1168
+ const run = await readActiveProject(ctx.cwd);
1219
1169
  if (!run || !toolNeedsApproval(event.toolName, event.input))
1220
1170
  return;
1221
1171
  const summary = toolApprovalSummary(event.toolName, event.input);
@@ -1247,65 +1197,145 @@ export default function psyclawExtension(pi) {
1247
1197
  return undefined;
1248
1198
  });
1249
1199
  pi.registerCommand("init", {
1250
- description: "初始化可追溯的研究项目",
1200
+ description: "搭建干净工作仓库(目录 + psyclaw.md),不自动追问",
1251
1201
  handler: async (args, ctx) => {
1252
1202
  try {
1253
1203
  const parsed = parseInitArgs(args);
1254
1204
  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");
1205
+ ctx.ui.notify([
1206
+ `工作仓库已初始化:${project.id}`,
1207
+ "已创建 data/raw|clean、analysis/、literature/、paper/、psyclaw.md、.psyclaw/",
1208
+ "Shift+Tab:chat → analysis → academic",
1209
+ "澄清与分析请切到 analysis;写作审稿请切到 academic。",
1210
+ ].join("\n"), "info");
1211
+ }
1212
+ catch (error) {
1213
+ await notifyError(ctx, error);
1214
+ }
1215
+ },
1216
+ });
1217
+ pi.registerCommand("verify", {
1218
+ description: "人确认关键字段:/verify list | /verify <id> verified|unverified|flagged",
1219
+ handler: async (args, ctx) => {
1220
+ try {
1221
+ const parts = args.trim().split(/\s+/).filter(Boolean);
1222
+ if (parts.length === 0 || parts[0] === "list") {
1223
+ const checklist = await loadVerifyChecklist(ctx.cwd);
1224
+ ctx.ui.notify(formatVerifyChecklist(checklist), "info");
1225
+ return;
1258
1226
  }
1259
- else {
1260
- ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm})。使用 /run 启动受控研究流程。`, "info");
1227
+ const id = parts[0];
1228
+ const status = (parts[1] ?? "verified");
1229
+ if (status !== "verified" && status !== "unverified" && status !== "flagged") {
1230
+ ctx.ui.notify("Usage: /verify list | /verify <id> verified|unverified|flagged [备注]", "info");
1231
+ return;
1261
1232
  }
1233
+ const notes = parts.slice(2).join(" ") || undefined;
1234
+ const checklist = await markVerifyItem(ctx.cwd, id, status, notes);
1235
+ ctx.ui.notify(formatVerifyChecklist(checklist), "info");
1262
1236
  }
1263
1237
  catch (error) {
1264
1238
  await notifyError(ctx, error);
1265
1239
  }
1266
1240
  },
1267
1241
  });
1268
- if (!legacyTestApi)
1269
- pi.registerCommand("run", {
1270
- description: "启动受控研究流程,可用 --skills a,b 选择本次优化 Skill",
1271
- handler: async (args, ctx) => {
1272
- try {
1273
- const parsed = parseRunArgs(args);
1274
- let project;
1275
- try {
1276
- project = await readProject(ctx.cwd);
1277
- }
1278
- catch {
1279
- ctx.ui.notify("当前目录还没有通过 /init 建立的研究记录,因此保持普通对话模式。需要受控研究流程时,请先运行 /init,再运行 /run。", "warning");
1242
+ pi.registerCommand("plan", {
1243
+ description: "分析方案:/plan [new|status|list|confirm|review|run|defer|handoff|reject]",
1244
+ handler: async (args, ctx) => {
1245
+ try {
1246
+ const trimmed = args.trim();
1247
+ const [cmdRaw, ...rest] = trimmed.split(/\s+/).filter(Boolean);
1248
+ const cmd = (cmdRaw ?? "status").toLowerCase();
1249
+ const usage = "Usage: /plan new [目标] | status | list | confirm [方法] | review | run | defer | handoff | reject [原因]";
1250
+ if (cmd === "new") {
1251
+ const goal = rest.join(" ").trim() || "从当前对话澄清的分析目标";
1252
+ const plan = await writeAnalysisPlan(ctx.cwd, createAnalysisPlan({ goal }));
1253
+ ctx.ui.notify(`${formatAnalysisPlanStatus(plan)}\n已创建。继续澄清/EDA,完成后用 /plan confirm。`, "info");
1254
+ return;
1255
+ }
1256
+ if (cmd === "list") {
1257
+ const ids = await listAnalysisPlans(ctx.cwd);
1258
+ ctx.ui.notify(ids.length > 0 ? `Plans:\n${ids.map((id) => `- ${id}`).join("\n")}` : "尚无 analysis/plans 记录。用 /plan new 开始。", "info");
1259
+ return;
1260
+ }
1261
+ let plan = await readActiveAnalysisPlan(ctx.cwd);
1262
+ if (!plan && cmd === "status") {
1263
+ ctx.ui.notify("没有活跃分析 Plan。用 /plan new [目标] 创建,或在 analysis 模式用自然语言触发 soft takeover。", "info");
1264
+ return;
1265
+ }
1266
+ if (!plan) {
1267
+ ctx.ui.notify(`没有活跃分析 Plan。\n${usage}`, "warning");
1268
+ return;
1269
+ }
1270
+ if (cmd === "status" || cmd === "show") {
1271
+ ctx.ui.notify(formatAnalysisPlanStatus(plan), "info");
1272
+ return;
1273
+ }
1274
+ if (cmd === "confirm") {
1275
+ const method = rest.join(" ").trim() || plan.primaryAnalysis || plan.proposedMethods[0] || "";
1276
+ if (!method) {
1277
+ ctx.ui.notify("请指定方法:/plan confirm <主分析方法>", "warning");
1280
1278
  return;
1281
1279
  }
1282
- const available = await selectableRunSkills(ctx.cwd);
1283
- const selectedSkills = resolveRunSkills(parsed.requestedSkills ?? [], available);
1284
- const objective = parsed.objective || project.goal;
1285
- const runId = `run_${Date.now()}`;
1286
- await appendApproval(ctx.cwd, {
1287
- kind: "run-mode",
1288
- nodeId: "run-mode",
1289
- decision: "approved",
1290
- actor: "human",
1291
- runId,
1292
- sha256: approvalInputDigest({ projectId: project.id, objective, selectedSkills, mode: parsed.mode }),
1293
- summary: "用户通过 /run 启动受控研究流程;计划内常规操作连续执行,仅研究取舍分歧请求决策",
1294
- });
1295
- await activateControlledRun(ctx.cwd, runId, project.id, objective, selectedSkills, parsed.mode);
1296
- await new RunEventLog(ctx.cwd, runId).append({ type: "planned", at: new Date().toISOString(), message: objective });
1297
- pi.appendEntry("psyclaw:controlled-run", { runId, projectId: project.id, objective, selectedSkills, mode: parsed.mode, activatedAt: new Date().toISOString() });
1298
- pi.sendUserMessage(controlledRunRequest(objective, selectedSkills, parsed.mode), ctx.isIdle() ? {} : { deliverAs: "followUp" });
1299
- ctx.ui.notify(`受控研究流程已启动${selectedSkills.length > 0 ? `;本次使用 Skill:${selectedSkills.join(", ")}` : ";使用默认 Skill"}。仅在研究分析存在需要你取舍的实质分歧时暂停询问。`, "info");
1280
+ plan = advanceAnalysisPlan(plan, { type: "confirm", method, backend: "local-script" });
1281
+ plan = advanceAnalysisPlan(plan, { type: "review" });
1282
+ plan = await writeAnalysisPlan(ctx.cwd, plan);
1283
+ ctx.ui.notify(`${formatAnalysisPlanStatus(plan)}\n软确认完成并已审查。接着 /plan run 或 /plan defer。`, "info");
1284
+ return;
1300
1285
  }
1301
- catch (error) {
1302
- await notifyError(ctx, error);
1286
+ if (cmd === "review") {
1287
+ plan = advanceAnalysisPlan(plan, { type: "review" });
1288
+ plan = await writeAnalysisPlan(ctx.cwd, plan);
1289
+ ctx.ui.notify(formatAnalysisPlanStatus(plan), "info");
1290
+ return;
1303
1291
  }
1304
- },
1305
- });
1306
- if (developerCommands || legacyTestApi)
1307
- pi.registerCommand("verify", {
1308
- description: "写入机器可读的交接检查点",
1292
+ if (cmd === "run") {
1293
+ plan = advanceAnalysisPlan(plan, { type: "run-now" });
1294
+ plan = await writeAnalysisPlan(ctx.cwd, plan);
1295
+ ctx.ui.notify(`${formatAnalysisPlanStatus(plan)}\n已标记立即执行:编写/运行 analysis/scripts/ 下的可复现脚本(特殊后端再用 MCP)。`, "info");
1296
+ pi.sendUserMessage([
1297
+ "/skill:analysis-plan",
1298
+ "",
1299
+ `Active plan ${plan.id} is marked running. Execute the confirmed method now with local reproducible scripts under analysis/scripts/.`,
1300
+ `Confirmed method: ${plan.confirmedMethod ?? plan.primaryAnalysis ?? "(see plan file)"}`,
1301
+ "Do not invent numbers. After results, update analysis/HANDOFF.md via /plan handoff.",
1302
+ ].join("\n"), ctx.isIdle() ? {} : { deliverAs: "followUp" });
1303
+ return;
1304
+ }
1305
+ if (cmd === "defer") {
1306
+ plan = advanceAnalysisPlan(plan, { type: "defer" });
1307
+ plan = await writeAnalysisPlan(ctx.cwd, plan);
1308
+ ctx.ui.notify(`${formatAnalysisPlanStatus(plan)}\n已推迟执行;稍后用 /plan run 继续。`, "info");
1309
+ return;
1310
+ }
1311
+ if (cmd === "handoff") {
1312
+ const path = await syncHandoffFromAnalysisPlan(ctx.cwd, plan);
1313
+ if (plan.status === "running") {
1314
+ plan = advanceAnalysisPlan(plan, plan.scriptEntrypoint
1315
+ ? { type: "complete", scriptEntrypoint: plan.scriptEntrypoint }
1316
+ : { type: "complete" });
1317
+ plan = await writeAnalysisPlan(ctx.cwd, plan);
1318
+ }
1319
+ ctx.ui.notify(`${formatAnalysisPlanStatus(plan)}\n已写入 ${path}。可切到 academic 模式。`, "info");
1320
+ return;
1321
+ }
1322
+ if (cmd === "reject") {
1323
+ const reason = rest.join(" ").trim() || "researcher rejected the proposal";
1324
+ plan = advanceAnalysisPlan(plan, { type: "block", reason });
1325
+ plan = await writeAnalysisPlan(ctx.cwd, plan);
1326
+ ctx.ui.notify(formatAnalysisPlanStatus(plan), "warning");
1327
+ return;
1328
+ }
1329
+ ctx.ui.notify(usage, "info");
1330
+ }
1331
+ catch (error) {
1332
+ await notifyError(ctx, error);
1333
+ }
1334
+ },
1335
+ });
1336
+ if (developerCommands)
1337
+ pi.registerCommand("handoff", {
1338
+ description: "(开发)写入机器可读交接检查点",
1309
1339
  handler: async (_args, ctx) => {
1310
1340
  try {
1311
1341
  const paths = projectPaths(ctx.cwd);
@@ -1321,29 +1351,13 @@ export default function psyclawExtension(pi) {
1321
1351
  verificationCommands: ["pnpm typecheck", "pnpm test"],
1322
1352
  generatedAt: new Date().toISOString(),
1323
1353
  });
1324
- ctx.ui.notify("Wrote notes/HANDOFF.md and notes/handoff.json", "info");
1354
+ ctx.ui.notify("Wrote .psyclaw/notes/HANDOFF.md and handoff.json", "info");
1325
1355
  }
1326
1356
  catch (error) {
1327
1357
  await notifyError(ctx, error);
1328
1358
  }
1329
1359
  },
1330
1360
  });
1331
- pi.registerCommand("brief", {
1332
- description: "运行离线证据门控研究简报",
1333
- handler: async (_args, ctx) => {
1334
- try {
1335
- if (!(await readControlledRun(ctx.cwd))) {
1336
- ctx.ui.notify("当前是普通对话模式。需要生成可追溯研究简报时,请先运行 /init,再运行 /run。", "warning");
1337
- return;
1338
- }
1339
- const result = await runOfflineBrief(ctx.cwd);
1340
- ctx.ui.notify(result.verdict === "pass" ? `Brief ready: ${result.briefPath}` : "Brief blocked by evidence gates", result.verdict === "pass" ? "info" : "warning");
1341
- }
1342
- catch (error) {
1343
- await notifyError(ctx, error);
1344
- }
1345
- },
1346
- });
1347
1361
  if (!legacyTestApi)
1348
1362
  pi.registerCommand("grill", {
1349
1363
  description: "逐题追问并压力测试学术研究方案",
@@ -1363,8 +1377,8 @@ export default function psyclawExtension(pi) {
1363
1377
  }
1364
1378
  try {
1365
1379
  const project = await readProject(ctx.cwd);
1366
- if (!(await readControlledRun(ctx.cwd))) {
1367
- ctx.ui.notify("请先使用 /run 启动受控研究流程;普通对话模式不会自动执行同行评审。", "warning");
1380
+ if (!(await readActiveProject(ctx.cwd))) {
1381
+ ctx.ui.notify("请先使用 /init 初始化研究项目;未建仓时不会自动执行同行评审。", "warning");
1368
1382
  return;
1369
1383
  }
1370
1384
  const reviewSkill = _args.trim();
@@ -1418,8 +1432,8 @@ export default function psyclawExtension(pi) {
1418
1432
  ctx.ui.notify("请先使用 /init 初始化研究项目,再运行 /loop", "warning");
1419
1433
  return;
1420
1434
  }
1421
- if (!(await readControlledRun(ctx.cwd))) {
1422
- ctx.ui.notify("项目已初始化,但尚未启动受控研究流程。请运行 /run 后再使用 /loop;当前继续保持普通对话模式。", "warning");
1435
+ if (!(await readActiveProject(ctx.cwd))) {
1436
+ ctx.ui.notify("请先使用 /init 初始化研究项目,再运行 /loop。", "warning");
1423
1437
  return;
1424
1438
  }
1425
1439
  const objective = requested || project.goal;
@@ -1427,7 +1441,7 @@ export default function psyclawExtension(pi) {
1427
1441
  "运行一个有边界的 PsyClaw 研究循环,采用计划、执行、检查、修正的顺序推进当前研究阶段。",
1428
1442
  `本轮目标:${objective}`,
1429
1443
  "开始前读取 .psyclaw/project.json、notes/goal.md 和 notes/plan.md;如果当前对话或项目中存在经研究者确认的 /grill 研究规格,以该规格细化初始计划,但不得违反系统规则、AGENTS.md 或研究门禁。",
1430
- "先识别当前最高优先级且前提已满足的一个任务,再使用可用工具实际推进。不要只描述准备做什么。普通项目文件写入、脚本和可恢复工具调用按 /run 的范围授权连续推进;原始数据覆盖、凭据、外部发布、破坏性操作仍遵守单独的安全边界。",
1444
+ "先识别当前最高优先级且前提已满足的一个任务,再使用可用工具实际推进。不要只描述准备做什么。普通项目文件写入、脚本和可恢复工具调用在已 /init 的项目中连续推进;原始数据覆盖、凭据、外部发布、破坏性操作仍遵守单独的安全边界。",
1431
1445
  "每完成一个实质步骤就检查产物、证据和计划是否一致。缺失信息若能检索、计算、重跑、改写或明确标注,应先自行修复。只有存在会改变研究问题、样本处理、操作化、估计目标、方法或解释的实质分歧时才请求研究者取舍;权限边界、不可恢复错误或当前阶段完成时停止。不得为了继续循环而虚构输入或结论。",
1432
1446
  "本次最多推进一个可验收的研究阶段。结束时简要报告已完成、已核验、阻塞、计划变化和下一步;规划内容不得表述为已有研究证据。",
1433
1447
  ].join("\n");
@@ -1502,47 +1516,69 @@ export default function psyclawExtension(pi) {
1502
1516
  return;
1503
1517
  }
1504
1518
  arsUiContext = ctx;
1505
- applyArsUiMode(true);
1519
+ applySessionMode("academic");
1506
1520
  return;
1507
1521
  }
1508
1522
  if (action === "status") {
1509
1523
  ctx.ui.notify([
1510
1524
  `PsyClaw ARS profile v${PSYCLAW_ARS_PROFILE_VERSION}`,
1511
- "模式:academic mode(Shift+Tab 切换;开启后直接对话)",
1512
- "Thinking:Ctrl+Shift+Tab(Shift+Tab 已让给 academic mode)",
1525
+ `当前模式:${arsModeEditor?.getMode() ?? sessionMode}(Shift+Tab:chat → analysis → academic)`,
1526
+ "Thinking:Ctrl+Shift+Tab",
1513
1527
  `来源:${ARS_REPOSITORY_URL} @ ${ARS_UPSTREAM_REF} (${ARS_UPSTREAM_COMMIT.slice(0, 12)})`,
1514
- "入口:Shift+Tab,/ars doctor,/ars start,/ars full <task>,/ars stop",
1528
+ "入口:Shift+Tab,/ars doctor,/ars start,/ars full <task>,/ars stop,/verify",
1515
1529
  formatNatureArsFillerStatus(natureFillersFromCommandContext(ctx)),
1516
1530
  ].join("\n"), "info");
1517
1531
  return;
1518
1532
  }
1519
1533
  if (action === "install") {
1520
- ctx.ui.notify("ARS 与 Nature/compose 技能已内置:按 Shift+Tab 进入 academic mode。", "info");
1534
+ ctx.ui.notify("ARS 与 Nature/compose 技能已内置:Shift+Tab 切到 academic。", "info");
1521
1535
  return;
1522
1536
  }
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");
1537
+ if (action === "doctor") {
1538
+ const activeTools = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
1539
+ const commands = typeof pi.getCommands === "function"
1540
+ ? pi.getCommands().map((command) => command.name)
1541
+ : [];
1542
+ const toolNames = [...new Set([
1543
+ ...activeTools.map((name) => String(name)),
1544
+ "psyclaw_ars_multi_agent",
1545
+ ])];
1546
+ const commandNames = [...new Set([...commands.map(String), "agents", "create-subagent"])];
1547
+ const report = await buildArsDoctorReport({
1548
+ repositoryRoot: arsRoot(),
1549
+ activeTools: toolNames,
1550
+ commands: commandNames,
1551
+ projectActive: Boolean(await readActiveProject(ctx.cwd)),
1552
+ });
1553
+ if (typeof pi.sendMessage === "function") {
1554
+ pi.sendMessage({ customType: "psyclaw-ars-doctor", content: report, display: true });
1555
+ }
1556
+ else {
1557
+ ctx.ui.notify(report, "info");
1558
+ }
1534
1559
  return;
1535
1560
  }
1536
1561
  if (action === "start") {
1537
1562
  arsUiContext = ctx;
1538
- applyArsUiMode(true, { syncSession: false });
1563
+ applySessionMode("academic");
1564
+ ctx.ui.notify("academic mode 已开启(Shift+Tab 可切换)", "info");
1565
+ return;
1539
1566
  }
1540
1567
  if (action === "stop") {
1541
1568
  arsUiContext = ctx;
1542
- applyArsUiMode(false, { syncSession: false });
1569
+ applySessionMode("chat");
1543
1570
  if (ctx.hasUI && isArsModeEditorText(ctx.ui.getEditorText?.() ?? "")) {
1544
1571
  ctx.ui.setEditorText("");
1545
1572
  }
1573
+ ctx.ui.notify("已回到 chat 模式", "info");
1574
+ return;
1575
+ }
1576
+ const upstreamCommand = action === "full"
1577
+ ? `/ars-full${rest.length > 0 ? ` ${rest.join(" ")}` : ""}`
1578
+ : undefined;
1579
+ if (!upstreamCommand) {
1580
+ ctx.ui.notify("Usage: /ars [status|doctor|start|full <task>|stop] 或按 Shift+Tab 切换 academic mode", "info");
1581
+ return;
1546
1582
  }
1547
1583
  // A queued prompt does not get a fresh system prompt in Pi. Wait so the
1548
1584
  // upstream wrapper can activate ARS and inject its compatibility note.
@@ -1758,10 +1794,10 @@ export default function psyclawExtension(pi) {
1758
1794
  }),
1759
1795
  executionMode: "sequential",
1760
1796
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1761
- const run = await readControlledRun(ctx.cwd);
1797
+ const run = await readActiveProject(ctx.cwd);
1762
1798
  if (!run) {
1763
1799
  return {
1764
- content: [{ type: "text", text: "当前是普通对话模式,不能创建研究决策状态。请仅在用户先执行 /init、再执行 /run 后使用本工具。" }],
1800
+ content: [{ type: "text", text: "当前目录尚未 /init,不能进入研究决策状态。请先执行 /init。" }],
1765
1801
  details: { status: "general-mode" },
1766
1802
  isError: true,
1767
1803
  };
@@ -1831,9 +1867,9 @@ export default function psyclawExtension(pi) {
1831
1867
  }),
1832
1868
  executionMode: "sequential",
1833
1869
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1834
- const run = await readControlledRun(ctx.cwd);
1870
+ const run = await readActiveProject(ctx.cwd);
1835
1871
  if (!run) {
1836
- return { content: [{ type: "text", text: "当前没有有效的受控研究流程。" }], details: { status: "general-mode" }, isError: true };
1872
+ return { content: [{ type: "text", text: "当前目录尚未 /init,没有有效的研究项目。" }], details: { status: "general-mode" }, isError: true };
1837
1873
  }
1838
1874
  const eventLog = new RunEventLog(ctx.cwd, run.runId);
1839
1875
  const events = await eventLog.snapshot();
@@ -1878,9 +1914,9 @@ export default function psyclawExtension(pi) {
1878
1914
  const servers = await runtimeMcps.list(ctx.cwd, params.server);
1879
1915
  return { content: [{ type: "text", text: JSON.stringify({ servers }, null, 2) }], details: { action: "list", servers: servers.length } };
1880
1916
  }
1881
- if (!(await readControlledRun(ctx.cwd))) {
1917
+ if (!(await readActiveProject(ctx.cwd))) {
1882
1918
  return {
1883
- content: [{ type: "text", text: "当前是普通对话模式。可以查看 MCP 工具,但实际调用需先运行 /init,再运行 /run。" }],
1919
+ content: [{ type: "text", text: "当前目录尚未 /init。可以查看 MCP 工具,但实际调用需先运行 /init。" }],
1884
1920
  details: { action: "call", status: "general-mode" },
1885
1921
  isError: true,
1886
1922
  };
@@ -1902,7 +1938,7 @@ export default function psyclawExtension(pi) {
1902
1938
  pi.registerTool({
1903
1939
  name: "psyclaw_ars_multi_agent",
1904
1940
  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.",
1941
+ 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
1942
  parameters: Type.Object({
1907
1943
  mode: Type.Union([Type.Literal("reviewer_full"), Type.Literal("reviewer_re_review")]),
1908
1944
  manuscriptPath: Type.Optional(Type.String()),
@@ -1941,6 +1977,21 @@ export default function psyclawExtension(pi) {
1941
1977
  }
1942
1978
  },
1943
1979
  });
1980
+ pi.registerTool({
1981
+ name: "psyclaw_ensure_pdf_engine",
1982
+ label: "Ensure PDF engine",
1983
+ 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.",
1984
+ parameters: Type.Object({}),
1985
+ executionMode: "sequential",
1986
+ async execute() {
1987
+ const result = await ensurePdfEngineForExport();
1988
+ return {
1989
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1990
+ details: result,
1991
+ ...(result.status === "missing" || result.status === "unsupported-platform" ? { isError: true } : {}),
1992
+ };
1993
+ },
1994
+ });
1944
1995
  pi.registerTool({
1945
1996
  name: "psyclaw_create",
1946
1997
  label: "Create project capability",
@@ -1996,7 +2047,7 @@ export default function psyclawExtension(pi) {
1996
2047
  label: "Research skill",
1997
2048
  description: "Load one trusted bundled psyclaw core skill and make its use visible to the user. Use this instead of directly reading a core SKILL.md file.",
1998
2049
  parameters: Type.Object({
1999
- name: Type.String({ description: "Core skill name: academic-grill, research-intake, evidence-capture, citation-audit, or research-brief" }),
2050
+ name: Type.String({ description: "Core skill name: academic-grill, research-intake, evidence-capture, citation-audit, or analysis-plan" }),
2000
2051
  purpose: Type.Optional(Type.String({ description: "Short user-facing reason for using this skill now" })),
2001
2052
  }),
2002
2053
  executionMode: "sequential",
@@ -2026,7 +2077,7 @@ export default function psyclawExtension(pi) {
2026
2077
  pi.registerTool({
2027
2078
  name: "psyclaw_workbench",
2028
2079
  label: "Research workbench",
2029
- description: "Primary psyclaw workbench for durable research state, evidence tracking, academic analysis/report contracts, separate operational authorization, and recoverable workflows. Infer the workflow from natural language, or pass an explicit `workflow` id to run it directly. Workflows: meta-analysis (real OpenAlex literature search, effect-size dataset contract at data/clean/effects.csv, R metafor delegation for REML / I² / Egger / forest — psyclaw never fabricates statistics), literature-review, analysis-delegation, writing-review, expert-review, institutional-fulltext. For paper/full-text requests call this first; for data analysis or academic reports route through research-intake -> evidence-capture -> citation-audit -> research-brief and record figure/document deliverables. Use psyclaw_research_decision only for qualifying substantive research trade-offs. Do not handle credentials.",
2080
+ description: "Primary psyclaw workbench for durable research state, evidence tracking, academic analysis/report contracts, separate operational authorization, and recoverable workflows. Infer the workflow from natural language, or pass an explicit `workflow` id to run it directly. Workflows: meta-analysis (real OpenAlex literature search, effect-size dataset contract at data/clean/effects.csv, R metafor delegation for REML / I² / Egger / forest — psyclaw never fabricates statistics), literature-review, analysis-delegation, writing-review, expert-review, institutional-fulltext. For paper/full-text requests call this first; for data analysis or academic reports route through research-intake -> evidence-capture -> citation-audit and record figure/document deliverables. Use psyclaw_research_decision only for qualifying substantive research trade-offs. Do not handle credentials.",
2030
2081
  parameters: Type.Object({
2031
2082
  request: Type.String({ description: "The user's research task in natural language" }),
2032
2083
  identifier: Type.Optional(Type.String({ description: "DOI, publisher URL, or exact paper title when relevant" })),
@@ -2037,21 +2088,24 @@ export default function psyclawExtension(pi) {
2037
2088
  executionMode: "sequential",
2038
2089
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
2039
2090
  try {
2091
+ // Soft gate: missing /init or verify does not hard-block results.
2092
+ const project = await readActiveProject(ctx.cwd);
2093
+ const softNote = project
2094
+ ? undefined
2095
+ : "尚未 /init(软门禁):继续执行;关键字段请 AI 核查并由人 /verify。建议先 /init 建仓。";
2040
2096
  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
2097
  const combined = `${request} ${params.identifier ?? ""}`;
2052
2098
  const explicitWorkflow = params.workflow?.trim();
2053
2099
  if (explicitWorkflow) {
2054
- return runWorkflowTool(ctx.cwd, explicitWorkflow, params.target, params.nStudies);
2100
+ const result = await runWorkflowTool(ctx.cwd, explicitWorkflow, params.target, params.nStudies);
2101
+ if (softNote && result && typeof result === "object" && "content" in result) {
2102
+ const text = Array.isArray(result.content) ? result.content.map((part) => part.text ?? "").join("\n") : "";
2103
+ return {
2104
+ ...result,
2105
+ content: [{ type: "text", text: `${softNote}\n${text}` }],
2106
+ };
2107
+ }
2108
+ return result;
2055
2109
  }
2056
2110
  const metaAnalysisIntent = /meta[- ]?analysis|meta分析|元分析|系统综述|系统评价|效应量|forest plot|funnel|publication bias/i.test(combined);
2057
2111
  const publishIntent = /publish|finalize|发布|导出|定稿|投稿|export (to )?(docx|word|apa)|docx export|apa ?7/i.test(combined);
@@ -2096,7 +2150,7 @@ export default function psyclawExtension(pi) {
2096
2150
  schemaVersion: "psyclaw/academic-report-plan/v1",
2097
2151
  workflow: "academic-analysis-report",
2098
2152
  status: "plan-required",
2099
- requiredSkills: ["research-intake", "evidence-capture", "citation-audit", "research-brief"],
2153
+ requiredSkills: ["research-intake", "evidence-capture", "citation-audit"],
2100
2154
  requiredDeliverables: ["analysis-plan", "source-backed-reference-list", "publication-ready-figures-or-reproducible-plot-script", "markdown-report", "docx-report"],
2101
2155
  documentExport: {
2102
2156
  primary: "pandoc",
@@ -2162,9 +2216,9 @@ export default function psyclawExtension(pi) {
2162
2216
  executionMode: "sequential",
2163
2217
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
2164
2218
  try {
2165
- if (!(await readControlledRun(ctx.cwd))) {
2219
+ if (!(await readActiveProject(ctx.cwd))) {
2166
2220
  return {
2167
- content: [{ type: "text", text: "当前是普通对话模式;请先运行 /init,再运行 /run,之后才会启用引用归档与论文证据门禁。" }],
2221
+ content: [{ type: "text", text: "当前目录尚未 /init;请先运行 /init,之后才会启用引用归档与论文证据门禁。" }],
2168
2222
  details: { status: "general-mode" },
2169
2223
  };
2170
2224
  }
@@ -2254,7 +2308,7 @@ export default function psyclawExtension(pi) {
2254
2308
  return;
2255
2309
  }
2256
2310
  // Do not let the runner create a partial `.psyclaw/runs` tree outside
2257
- // an explicitly activated `/init` -> `/run` research workflow.
2311
+ // an explicitly initialized `/init` research project.
2258
2312
  try {
2259
2313
  await readProject(ctx.cwd);
2260
2314
  }