psyclaw 0.29.1 → 0.29.3

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 (58) hide show
  1. package/README.md +8 -10
  2. package/agents/recommended/catalog.json +2 -2
  3. package/dist/apps/panel/index.html +2100 -54
  4. package/dist/src/adapters/pi/extension.js +156 -196
  5. package/dist/src/adapters/pi/extension.js.map +1 -1
  6. package/dist/src/analysis/plan.d.ts +82 -0
  7. package/dist/src/analysis/plan.js +246 -0
  8. package/dist/src/analysis/plan.js.map +1 -0
  9. package/dist/src/analysis/stats-router.d.ts +13 -0
  10. package/dist/src/analysis/stats-router.js +44 -0
  11. package/dist/src/analysis/stats-router.js.map +1 -0
  12. package/dist/src/ars/academic-router.d.ts +33 -0
  13. package/dist/src/ars/academic-router.js +204 -0
  14. package/dist/src/ars/academic-router.js.map +1 -0
  15. package/dist/src/ars/doctor.d.ts +2 -2
  16. package/dist/src/ars/doctor.js +4 -4
  17. package/dist/src/ars/doctor.js.map +1 -1
  18. package/dist/src/ars/profile.js +1 -1
  19. package/dist/src/ars/profile.js.map +1 -1
  20. package/dist/src/branding.js +7 -7
  21. package/dist/src/branding.js.map +1 -1
  22. package/dist/src/cli.js +1 -8
  23. package/dist/src/cli.js.map +1 -1
  24. package/dist/src/index.d.ts +2 -1
  25. package/dist/src/index.js +2 -1
  26. package/dist/src/index.js.map +1 -1
  27. package/dist/src/panel/extension.js +14 -5
  28. package/dist/src/panel/extension.js.map +1 -1
  29. package/dist/src/panel/server.js +40 -2
  30. package/dist/src/panel/server.js.map +1 -1
  31. package/dist/src/project/approvals.d.ts +4 -3
  32. package/dist/src/project/approvals.js +4 -3
  33. package/dist/src/project/approvals.js.map +1 -1
  34. package/dist/src/project/bootstrap.d.ts +1 -1
  35. package/dist/src/project/bootstrap.js +1 -1
  36. package/dist/src/project/paths.d.ts +1 -0
  37. package/dist/src/project/paths.js +2 -0
  38. package/dist/src/project/paths.js.map +1 -1
  39. package/dist/src/project/workspace.d.ts +1 -1
  40. package/dist/src/project/workspace.js +5 -5
  41. package/dist/src/project/workspace.js.map +1 -1
  42. package/dist/src/session/modes.d.ts +1 -1
  43. package/dist/src/session/modes.js +9 -4
  44. package/dist/src/session/modes.js.map +1 -1
  45. package/dist/src/skills/recommended.js +1 -1
  46. package/dist/src/skills/recommended.js.map +1 -1
  47. package/dist/src/style/cli-ui.js +3 -3
  48. package/dist/src/style/cli-ui.js.map +1 -1
  49. package/package.json +2 -1
  50. package/scripts/rebrand-pi.mjs +6 -4
  51. package/skills/core/academic-grill/SKILL.md +1 -1
  52. package/skills/core/analysis-plan/SKILL.md +29 -0
  53. package/skills/core/manifest.json +3 -3
  54. package/vendor/ars/pi/wrapper.js +1 -1
  55. package/dist/src/research/brief.d.ts +0 -11
  56. package/dist/src/research/brief.js +0 -81
  57. package/dist/src/research/brief.js.map +0 -1
  58. 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";
@@ -22,8 +22,11 @@ import { RuntimeMcpRegistry, setUserMcpConfigEnabled, } from "../../integrations
22
22
  import { SecretInputComponent, ProviderPickerComponent, } from "../../tui/provider-picker.js";
23
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
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";
25
28
  import { arsRoot } from "../../ars/pi-panel-executor.js";
26
- import { ArsModeEditor, ARS_MODE_STATUS, isArsModeEditorText } from "../../ars/mode-editor.js";
29
+ import { ArsModeEditor, isArsModeEditorText } from "../../ars/mode-editor.js";
27
30
  import { MODE_STATUS, nextSessionMode, parseSessionMode, sessionModePrompt, } from "../../session/modes.js";
28
31
  import { formatVerifyChecklist, loadVerifyChecklist, markVerifyItem } from "../../verify/checklist.js";
29
32
  const PARADIGMS = new Set([
@@ -67,7 +70,7 @@ async function runPluginCommand(args) {
67
70
  throw new Error(`不支持的 Plugin 操作:${args[0] ?? ""}`);
68
71
  }
69
72
  const activeAgentRuns = new Set();
70
- 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"]);
71
74
  function findLastMatching(items, predicate) {
72
75
  for (let index = items.length - 1; index >= 0; index -= 1) {
73
76
  const item = items[index];
@@ -200,80 +203,19 @@ function academicGrillRequest(subject, mode) {
200
203
  : "持久化模式:review。追问完成后先展示拟更新摘要和受影响文件,并询问我是否写回;得到明确确认前不得修改项目文档。",
201
204
  ].join("\n");
202
205
  }
203
- async function controlledRunPath(root) {
204
- return join(projectPaths(root).root, ".psyclaw", "controlled-run.json");
205
- }
206
- async function readControlledRun(root) {
206
+ async function readActiveProject(root) {
207
207
  try {
208
- const value = JSON.parse(await readFile(await controlledRunPath(root), "utf8"));
209
- 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"))
210
- return null;
211
208
  const project = await readProject(root);
212
- if (project.id !== value.projectId)
213
- return null;
214
- const authorization = findLastMatching(await readApprovals(root), (record) => record.kind === "run-mode" && record.runId === value.runId);
215
- if (authorization?.decision !== "approved" || authorization.actor !== "human")
216
- return null;
217
- 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
+ };
218
214
  }
219
215
  catch {
220
216
  return null;
221
217
  }
222
218
  }
223
- async function activateControlledRun(root, runId, projectId, objective, selectedSkills, mode) {
224
- const state = {
225
- schemaVersion: "psyclaw/controlled-run/v1",
226
- runId,
227
- projectId,
228
- objective,
229
- selectedSkills: [...new Set(selectedSkills)],
230
- mode,
231
- activatedAt: new Date().toISOString(),
232
- status: "active",
233
- };
234
- await atomicWriteFile(await controlledRunPath(root), `${JSON.stringify(state, null, 2)}\n`);
235
- return state;
236
- }
237
- function controlledRunRequest(objective, selectedSkills, mode) {
238
- return [
239
- `PsyClaw 受控研究流程已由用户通过 /run 明确启动${mode === "auto" ? ",本次采用连续执行方式" : ""}。先读取 .psyclaw/project.json、.psyclaw/controlled-run.json、.psyclaw/approvals.jsonl、notes/research-spec.md 和 notes/plan.md。`,
240
- `本次目标:${objective}`,
241
- selectedSkills.length > 0
242
- ? `本次运行由用户选择的优化 Skill:${selectedSkills.join(", ")}。在相关任务中优先加载并遵循这些 Skill;同名 Skill 只加载一次。`
243
- : "本次运行未指定额外 Skill,使用 PsyClaw 默认研究流程。不要删除、停用或改写已安装的其他 Skill。",
244
- "计划内的文件创建、脚本执行、分析委托、可恢复下载和普通工具调用连续推进,不要逐项向用户索要许可;/run 已代表用户同意这些为完成本次目标所必需且符合项目边界的操作,系统仍须记录结构化回执。凭据处理、原始数据覆盖、破坏性命令、绕过访问控制和外部发布不在该授权范围内。",
245
- "人工决策只用于真正的研究取舍:存在两个以上有实质依据的合理方案,现有研究问题、数据、证据和方法规范不能确定其中之一,且不同选择会改变研究问题、样本纳入或排除、变量操作化、估计目标、分析方法或结果解释。提出决策前必须先核对可用证据与通行方法;提出时说明仍无法消解的原因、备选方案、各自依据与影响,并给出系统推荐;一次只请求一个决定。",
246
- "满足上述全部条件时,先调用 psyclaw_research_decision 写入结构化研究取舍事件,再向研究者提问。该工具拒绝的事项不得改用自然语言伪装成 awaiting-human;应继续自动修复、明确报告技术限制,或按独立安全授权边界处理。",
247
- "不要因软件缺失、安装配置、格式问题、脚本错误、字段可推断、统计报告遗漏、效应量或置信区间缺失、缺失值说明不足、引用格式错误或可补充的证据不足而进入人工决策。先自行安装或修复、重跑、补算、补写、检索核验或缩小表述;确实无法完成时报告具体未完成项和下一步,不把技术失败包装成研究者取舍。",
248
- "实际推进当前尚未完成的研究任务;先形成或更新符合学术规范的 Markdown 分析报告,不能只描述计划。分析报告必须清楚区分数据来源、统计结果、文献证据、限制与尚未核验内容。",
249
- "写作时把内部状态和校验术语留在 .psyclaw 记录中,不要把 Claim、Evidence、ledger、gate、audit、blocked、receipt 等标签直接写进面向研究者的对话、摘要或论文。中文正文避免生硬使用“审计”“结局”“依赖”“受阻”等词:分别按语境改为质量检查、结果变量或指标、所需软件或前提、无法分析。",
250
- "摘要首先服从用户提供的会议或期刊格式;没有明确要求时写成一段式摘要,完整交代目的、方法、主要结果和结论,但不机械添加四个小标题。摘要只保留最关键结果,不罗列损坏列、逐列缺失数、文件哈希或实现细节。数据质量细节放在方法、附录或单独质量报告中。",
251
- "若用户只提供数据集,先结合变量、研究背景和可识别的理论机制主动提出 2-4 个有研究价值且可检验的问题或假设,再推荐分析主线。不要把“是否有预注册”作为第一个或默认问题;探索性分析明确标注即可,只有用户声明为确证性研究时才核对事前分析方案,且不索要公开链接。",
252
- "数据与隐私默认最小披露:对话、报告和论文先给汇总缺失率、受影响的变量组及其分析影响,不输出原始行、个体文本、标识符、完整列清单或冗长的逐列缺失信息;确需列名时只列与当前方法决策直接相关者,完整诊断另存补充材料。",
253
- "参考文献按目标格式生成并逐项核对。APA 7 稿件必须做到正文引文与文末条目一一对应,区分期刊论文、会议论文、数据集、预印本和软件,核对作者顺序、年份、标题、刊名或会议名、卷期页码或文章号、DOI、标点、排序及悬挂缩进;缺少字段时不得猜测。",
254
- "分析报告完成后,下一步不是直接询问是否导出 DOCX,而是询问用户是否据此撰写论文。",
255
- "若用户选择撰写论文,再单独询问是否先进行文献调研,并说明该阶段可能花费较长时间;提示用户可以指定已启用的 Skill,或者选择 PsyClaw 默认的文献调研方式。不要在获得答复前自动开始长时调研。",
256
- "文献调研完成并核验后,再询问是否进行全文撰写,并询问使用哪个写作 Skill;未指定时可以建议默认方案,但仍需用户确认。只有充分证据经过核验后,才能把相关主张写入论文正文。",
257
- "同行评审由用户需要时另行运行 /review;不要在本轮自动审稿。/review 会再次询问使用哪个评审 Skill。最后才询问是否导出 DOCX,以及格式要求。",
258
- "开放获取 PDF 自动保存到 literature/pdfs/。不得绕过付费墙;没有合法开放全文时给出 https://doi.org/<DOI> 可点击链接,提示用户通过自己的权限下载到系统给出的目标路径。论文中的每篇引用都必须完成 DOI 核验并有本地 PDF,否则用自然语言列出待补材料,并说明补齐前不宜定稿;不要向用户输出 blocked、gate 等内部状态标签。",
259
- "如果用户在未完成文献调研、全文写作或评审前要求导出 DOCX,允许导出当前分析报告,但必须明确标为遵循学术规范的分析报告,不得称为论文。",
260
- ].join("\n");
261
- }
262
- function parseRunArgs(args) {
263
- const value = args.trim();
264
- const auto = value === "auto" || value.startsWith("auto ");
265
- const rest = auto ? value.slice(4).trim() : value;
266
- if (!rest.startsWith("--skills"))
267
- return { objective: rest, mode: auto ? "auto" : "human" };
268
- const match = rest.match(/^--skills(?:=|\s+)([^\s]+)(?:\s+([\s\S]*))?$/);
269
- if (!match)
270
- throw new Error("Usage: /run [auto] [--skills skill-a,skill-b] [objective]");
271
- return {
272
- objective: (match[2] ?? "").trim(),
273
- requestedSkills: [...new Set(match[1].split(",").map((item) => item.trim()).filter(Boolean))],
274
- mode: auto ? "auto" : "human",
275
- };
276
- }
277
219
  function contextProjectTrusted(ctx) {
278
220
  const candidate = ctx;
279
221
  return typeof candidate.isProjectTrusted === "function" && candidate.isProjectTrusted();
@@ -294,41 +236,6 @@ function requiresSeparateOperationConfirmation(toolName, input) {
294
236
  const text = `${toolName} ${JSON.stringify(input ?? {})}`;
295
237
  return /submit\s+(?:manuscript|paper|form)|投稿(?:到|至)|发布到(?:外部|期刊|会议|网站)|send\s+(?:email|message)|邮件发送/i.test(text);
296
238
  }
297
- async function selectableRunSkills(root) {
298
- const state = await readRecommendationState(root);
299
- const rows = await skillManagerRows(root, state);
300
- const available = [
301
- ...coreSkillNames().map((name) => ({ id: name, name })),
302
- ...rows.filter((row) => row.installed && row.enabled && !row.blocked).map((row) => ({ id: row.id, name: row.name })),
303
- ];
304
- const seen = new Set();
305
- return available.filter((skill) => {
306
- const key = skill.name.trim().toLocaleLowerCase();
307
- if (!key || seen.has(key))
308
- return false;
309
- seen.add(key);
310
- return true;
311
- });
312
- }
313
- function resolveRunSkills(requested, available) {
314
- const lookup = new Map();
315
- for (const skill of available) {
316
- lookup.set(skill.id.toLocaleLowerCase(), skill.name);
317
- lookup.set(skill.name.toLocaleLowerCase(), skill.name);
318
- }
319
- const selected = [];
320
- const unknown = [];
321
- for (const value of requested) {
322
- const name = lookup.get(value.toLocaleLowerCase());
323
- if (!name)
324
- unknown.push(value);
325
- else if (!selected.some((item) => item.toLocaleLowerCase() === name.toLocaleLowerCase()))
326
- selected.push(name);
327
- }
328
- if (unknown.length > 0)
329
- throw new Error(`本次运行不可用的 Skill:${unknown.join(", ")}。请先通过 /skill 安装或启用。`);
330
- return selected;
331
- }
332
239
  async function recommendedItems(kind) {
333
240
  const file = kind === "skills" ? "catalog.json" : "mcp-catalog.json";
334
241
  const moduleDir = dirname(fileURLToPath(import.meta.url));
@@ -1129,11 +1036,8 @@ export default function psyclawExtension(pi) {
1129
1036
  sessionMode = mode;
1130
1037
  arsModeEditor?.setMode(mode);
1131
1038
  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);
1039
+ // Clear legacy ars status key so it never duplicates with "mode".
1040
+ arsUiContext?.ui.setStatus("ars", undefined);
1137
1041
  if (opts?.syncSession === false || !arsUiContext)
1138
1042
  return;
1139
1043
  setArsPiSessionActive(typeof pi.appendEntry === "function"
@@ -1174,7 +1078,7 @@ export default function psyclawExtension(pi) {
1174
1078
  pi.on("session_shutdown", () => runtimeMcps.close());
1175
1079
  if (!legacyTestApi && typeof pi.on === "function")
1176
1080
  pi.on("before_agent_start", async (event, ctx) => {
1177
- if (!(await readControlledRun(ctx?.cwd ?? process.cwd())))
1081
+ if (!(await readActiveProject(ctx?.cwd ?? process.cwd())))
1178
1082
  return;
1179
1083
  const prompt = userRulesPrompt(await loadUserRules(ctx?.cwd ?? process.cwd()));
1180
1084
  if (prompt)
@@ -1208,10 +1112,7 @@ export default function psyclawExtension(pi) {
1208
1112
  editor.onModeChange = (mode) => {
1209
1113
  sessionMode = mode;
1210
1114
  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);
1115
+ ctx.ui.setStatus("ars", undefined);
1215
1116
  };
1216
1117
  editor.onCycleMode = () => {
1217
1118
  cycleSessionMode();
@@ -1222,8 +1123,7 @@ export default function psyclawExtension(pi) {
1222
1123
  editor.setMode(restore);
1223
1124
  sessionMode = restore;
1224
1125
  ctx.ui.setStatus("mode", MODE_STATUS[restore]);
1225
- if (restore === "academic")
1226
- ctx.ui.setStatus("ars", ARS_MODE_STATUS);
1126
+ ctx.ui.setStatus("ars", undefined);
1227
1127
  return editor;
1228
1128
  });
1229
1129
  });
@@ -1231,14 +1131,34 @@ export default function psyclawExtension(pi) {
1231
1131
  pi.on("input", (event, ctx) => {
1232
1132
  if (event.source === "extension")
1233
1133
  return;
1234
- if (!/^\/(?:login|logout)(?:\s|$)/i.test(event.text.trim()))
1235
- return;
1236
- ctx.ui.notify("PsyClaw 已统一隐藏底层登录命令。请使用 /provider 配置、切换或更新模型凭据。", "info");
1237
- return { action: "handled" };
1134
+ if (/^\/(?:login|logout)(?:\s|$)/i.test(event.text.trim())) {
1135
+ ctx.ui.notify("PsyClaw 已统一隐藏底层登录命令。请使用 /provider 配置、切换或更新模型凭据。", "info");
1136
+ return { action: "handled" };
1137
+ }
1138
+ const mode = arsModeEditor?.getMode() ?? sessionMode;
1139
+ if (mode === "academic") {
1140
+ const match = resolveAcademicSoftRoute(event.text);
1141
+ if (!match)
1142
+ return;
1143
+ const ranked = rankAcademicSoftRoutes(event.text, 3);
1144
+ return {
1145
+ action: "transform",
1146
+ text: formatAcademicSoftRouteInvocation(match, event.text, ranked.slice(1)),
1147
+ };
1148
+ }
1149
+ if (mode === "analysis") {
1150
+ const match = resolveStatsIntent(event.text);
1151
+ if (!match)
1152
+ return;
1153
+ return {
1154
+ action: "transform",
1155
+ text: formatAnalysisPlanTakeover(event.text),
1156
+ };
1157
+ }
1238
1158
  });
1239
1159
  if (!legacyTestApi && typeof pi.on === "function")
1240
1160
  pi.on("tool_call", async (event, ctx) => {
1241
- const run = await readControlledRun(ctx.cwd);
1161
+ const run = await readActiveProject(ctx.cwd);
1242
1162
  if (!run || !toolNeedsApproval(event.toolName, event.input))
1243
1163
  return;
1244
1164
  const summary = toolApprovalSummary(event.toolName, event.input);
@@ -1270,7 +1190,7 @@ export default function psyclawExtension(pi) {
1270
1190
  return undefined;
1271
1191
  });
1272
1192
  pi.registerCommand("init", {
1273
- description: "搭建干净工作仓库(目录 + psyclaw.md),不自动追问或启动 /run",
1193
+ description: "搭建干净工作仓库(目录 + psyclaw.md),不自动追问",
1274
1194
  handler: async (args, ctx) => {
1275
1195
  try {
1276
1196
  const parsed = parseInitArgs(args);
@@ -1279,7 +1199,7 @@ export default function psyclawExtension(pi) {
1279
1199
  `工作仓库已初始化:${project.id}`,
1280
1200
  "已创建 data/raw|clean、analysis/、literature/、paper/、psyclaw.md、.psyclaw/",
1281
1201
  "Shift+Tab:chat → analysis → academic",
1282
- "澄清与分析请切到 analysis;写作审稿请切到 academic。可选 /run 加强受控工具。",
1202
+ "澄清与分析请切到 analysis;写作审稿请切到 academic。",
1283
1203
  ].join("\n"), "info");
1284
1204
  }
1285
1205
  catch (error) {
@@ -1312,44 +1232,100 @@ export default function psyclawExtension(pi) {
1312
1232
  }
1313
1233
  },
1314
1234
  });
1315
- if (!legacyTestApi)
1316
- pi.registerCommand("run", {
1317
- description: "启动受控研究流程,可用 --skills a,b 选择本次优化 Skill",
1318
- handler: async (args, ctx) => {
1319
- try {
1320
- const parsed = parseRunArgs(args);
1321
- let project;
1322
- try {
1323
- project = await readProject(ctx.cwd);
1324
- }
1325
- catch {
1326
- ctx.ui.notify("当前目录还没有通过 /init 建立的研究记录。可先 /init 建仓;需要更密受控工具链时再 /run。", "warning");
1235
+ pi.registerCommand("plan", {
1236
+ description: "分析方案:/plan [new|status|list|confirm|review|run|defer|handoff|reject]",
1237
+ handler: async (args, ctx) => {
1238
+ try {
1239
+ const trimmed = args.trim();
1240
+ const [cmdRaw, ...rest] = trimmed.split(/\s+/).filter(Boolean);
1241
+ const cmd = (cmdRaw ?? "status").toLowerCase();
1242
+ const usage = "Usage: /plan new [目标] | status | list | confirm [方法] | review | run | defer | handoff | reject [原因]";
1243
+ if (cmd === "new") {
1244
+ const goal = rest.join(" ").trim() || "从当前对话澄清的分析目标";
1245
+ const plan = await writeAnalysisPlan(ctx.cwd, createAnalysisPlan({ goal }));
1246
+ ctx.ui.notify(`${formatAnalysisPlanStatus(plan)}\n已创建。继续澄清/EDA,完成后用 /plan confirm。`, "info");
1247
+ return;
1248
+ }
1249
+ if (cmd === "list") {
1250
+ const ids = await listAnalysisPlans(ctx.cwd);
1251
+ ctx.ui.notify(ids.length > 0 ? `Plans:\n${ids.map((id) => `- ${id}`).join("\n")}` : "尚无 analysis/plans 记录。用 /plan new 开始。", "info");
1252
+ return;
1253
+ }
1254
+ let plan = await readActiveAnalysisPlan(ctx.cwd);
1255
+ if (!plan && cmd === "status") {
1256
+ ctx.ui.notify("没有活跃分析 Plan。用 /plan new [目标] 创建,或在 analysis 模式用自然语言触发 soft takeover。", "info");
1257
+ return;
1258
+ }
1259
+ if (!plan) {
1260
+ ctx.ui.notify(`没有活跃分析 Plan。\n${usage}`, "warning");
1261
+ return;
1262
+ }
1263
+ if (cmd === "status" || cmd === "show") {
1264
+ ctx.ui.notify(formatAnalysisPlanStatus(plan), "info");
1265
+ return;
1266
+ }
1267
+ if (cmd === "confirm") {
1268
+ const method = rest.join(" ").trim() || plan.primaryAnalysis || plan.proposedMethods[0] || "";
1269
+ if (!method) {
1270
+ ctx.ui.notify("请指定方法:/plan confirm <主分析方法>", "warning");
1327
1271
  return;
1328
1272
  }
1329
- const available = await selectableRunSkills(ctx.cwd);
1330
- const selectedSkills = resolveRunSkills(parsed.requestedSkills ?? [], available);
1331
- const objective = parsed.objective || project.goal;
1332
- const runId = `run_${Date.now()}`;
1333
- await appendApproval(ctx.cwd, {
1334
- kind: "run-mode",
1335
- nodeId: "run-mode",
1336
- decision: "approved",
1337
- actor: "human",
1338
- runId,
1339
- sha256: approvalInputDigest({ projectId: project.id, objective, selectedSkills, mode: parsed.mode }),
1340
- summary: "用户通过 /run 启动受控研究流程;计划内常规操作连续执行,仅研究取舍分歧请求决策",
1341
- });
1342
- await activateControlledRun(ctx.cwd, runId, project.id, objective, selectedSkills, parsed.mode);
1343
- await new RunEventLog(ctx.cwd, runId).append({ type: "planned", at: new Date().toISOString(), message: objective });
1344
- pi.appendEntry("psyclaw:controlled-run", { runId, projectId: project.id, objective, selectedSkills, mode: parsed.mode, activatedAt: new Date().toISOString() });
1345
- pi.sendUserMessage(controlledRunRequest(objective, selectedSkills, parsed.mode), ctx.isIdle() ? {} : { deliverAs: "followUp" });
1346
- ctx.ui.notify(`受控研究流程已启动${selectedSkills.length > 0 ? `;本次使用 Skill:${selectedSkills.join(", ")}` : ";使用默认 Skill"}。仅在研究分析存在需要你取舍的实质分歧时暂停询问。`, "info");
1273
+ plan = advanceAnalysisPlan(plan, { type: "confirm", method, backend: "local-script" });
1274
+ plan = advanceAnalysisPlan(plan, { type: "review" });
1275
+ plan = await writeAnalysisPlan(ctx.cwd, plan);
1276
+ ctx.ui.notify(`${formatAnalysisPlanStatus(plan)}\n软确认完成并已审查。接着 /plan run 或 /plan defer。`, "info");
1277
+ return;
1347
1278
  }
1348
- catch (error) {
1349
- await notifyError(ctx, error);
1279
+ if (cmd === "review") {
1280
+ plan = advanceAnalysisPlan(plan, { type: "review" });
1281
+ plan = await writeAnalysisPlan(ctx.cwd, plan);
1282
+ ctx.ui.notify(formatAnalysisPlanStatus(plan), "info");
1283
+ return;
1350
1284
  }
1351
- },
1352
- });
1285
+ if (cmd === "run") {
1286
+ plan = advanceAnalysisPlan(plan, { type: "run-now" });
1287
+ plan = await writeAnalysisPlan(ctx.cwd, plan);
1288
+ ctx.ui.notify(`${formatAnalysisPlanStatus(plan)}\n已标记立即执行:编写/运行 analysis/scripts/ 下的可复现脚本(特殊后端再用 MCP)。`, "info");
1289
+ pi.sendUserMessage([
1290
+ "/skill:analysis-plan",
1291
+ "",
1292
+ `Active plan ${plan.id} is marked running. Execute the confirmed method now with local reproducible scripts under analysis/scripts/.`,
1293
+ `Confirmed method: ${plan.confirmedMethod ?? plan.primaryAnalysis ?? "(see plan file)"}`,
1294
+ "Do not invent numbers. After results, update analysis/HANDOFF.md via /plan handoff.",
1295
+ ].join("\n"), ctx.isIdle() ? {} : { deliverAs: "followUp" });
1296
+ return;
1297
+ }
1298
+ if (cmd === "defer") {
1299
+ plan = advanceAnalysisPlan(plan, { type: "defer" });
1300
+ plan = await writeAnalysisPlan(ctx.cwd, plan);
1301
+ ctx.ui.notify(`${formatAnalysisPlanStatus(plan)}\n已推迟执行;稍后用 /plan run 继续。`, "info");
1302
+ return;
1303
+ }
1304
+ if (cmd === "handoff") {
1305
+ const path = await syncHandoffFromAnalysisPlan(ctx.cwd, plan);
1306
+ if (plan.status === "running") {
1307
+ plan = advanceAnalysisPlan(plan, plan.scriptEntrypoint
1308
+ ? { type: "complete", scriptEntrypoint: plan.scriptEntrypoint }
1309
+ : { type: "complete" });
1310
+ plan = await writeAnalysisPlan(ctx.cwd, plan);
1311
+ }
1312
+ ctx.ui.notify(`${formatAnalysisPlanStatus(plan)}\n已写入 ${path}。可切到 academic 模式。`, "info");
1313
+ return;
1314
+ }
1315
+ if (cmd === "reject") {
1316
+ const reason = rest.join(" ").trim() || "researcher rejected the proposal";
1317
+ plan = advanceAnalysisPlan(plan, { type: "block", reason });
1318
+ plan = await writeAnalysisPlan(ctx.cwd, plan);
1319
+ ctx.ui.notify(formatAnalysisPlanStatus(plan), "warning");
1320
+ return;
1321
+ }
1322
+ ctx.ui.notify(usage, "info");
1323
+ }
1324
+ catch (error) {
1325
+ await notifyError(ctx, error);
1326
+ }
1327
+ },
1328
+ });
1353
1329
  if (developerCommands)
1354
1330
  pi.registerCommand("handoff", {
1355
1331
  description: "(开发)写入机器可读交接检查点",
@@ -1375,22 +1351,6 @@ export default function psyclawExtension(pi) {
1375
1351
  }
1376
1352
  },
1377
1353
  });
1378
- pi.registerCommand("brief", {
1379
- description: "运行离线证据门控研究简报",
1380
- handler: async (_args, ctx) => {
1381
- try {
1382
- if (!(await readControlledRun(ctx.cwd))) {
1383
- ctx.ui.notify("当前是普通对话模式。需要生成可追溯研究简报时,请先运行 /init,再运行 /run。", "warning");
1384
- return;
1385
- }
1386
- const result = await runOfflineBrief(ctx.cwd);
1387
- ctx.ui.notify(result.verdict === "pass" ? `Brief ready: ${result.briefPath}` : "Brief blocked by evidence gates", result.verdict === "pass" ? "info" : "warning");
1388
- }
1389
- catch (error) {
1390
- await notifyError(ctx, error);
1391
- }
1392
- },
1393
- });
1394
1354
  if (!legacyTestApi)
1395
1355
  pi.registerCommand("grill", {
1396
1356
  description: "逐题追问并压力测试学术研究方案",
@@ -1410,8 +1370,8 @@ export default function psyclawExtension(pi) {
1410
1370
  }
1411
1371
  try {
1412
1372
  const project = await readProject(ctx.cwd);
1413
- if (!(await readControlledRun(ctx.cwd))) {
1414
- ctx.ui.notify("请先使用 /run 启动受控研究流程;普通对话模式不会自动执行同行评审。", "warning");
1373
+ if (!(await readActiveProject(ctx.cwd))) {
1374
+ ctx.ui.notify("请先使用 /init 初始化研究项目;未建仓时不会自动执行同行评审。", "warning");
1415
1375
  return;
1416
1376
  }
1417
1377
  const reviewSkill = _args.trim();
@@ -1465,8 +1425,8 @@ export default function psyclawExtension(pi) {
1465
1425
  ctx.ui.notify("请先使用 /init 初始化研究项目,再运行 /loop", "warning");
1466
1426
  return;
1467
1427
  }
1468
- if (!(await readControlledRun(ctx.cwd))) {
1469
- ctx.ui.notify("项目已初始化,但尚未启动受控研究流程。请运行 /run 后再使用 /loop;当前继续保持普通对话模式。", "warning");
1428
+ if (!(await readActiveProject(ctx.cwd))) {
1429
+ ctx.ui.notify("请先使用 /init 初始化研究项目,再运行 /loop。", "warning");
1470
1430
  return;
1471
1431
  }
1472
1432
  const objective = requested || project.goal;
@@ -1474,7 +1434,7 @@ export default function psyclawExtension(pi) {
1474
1434
  "运行一个有边界的 PsyClaw 研究循环,采用计划、执行、检查、修正的顺序推进当前研究阶段。",
1475
1435
  `本轮目标:${objective}`,
1476
1436
  "开始前读取 .psyclaw/project.json、notes/goal.md 和 notes/plan.md;如果当前对话或项目中存在经研究者确认的 /grill 研究规格,以该规格细化初始计划,但不得违反系统规则、AGENTS.md 或研究门禁。",
1477
- "先识别当前最高优先级且前提已满足的一个任务,再使用可用工具实际推进。不要只描述准备做什么。普通项目文件写入、脚本和可恢复工具调用按 /run 的范围授权连续推进;原始数据覆盖、凭据、外部发布、破坏性操作仍遵守单独的安全边界。",
1437
+ "先识别当前最高优先级且前提已满足的一个任务,再使用可用工具实际推进。不要只描述准备做什么。普通项目文件写入、脚本和可恢复工具调用在已 /init 的项目中连续推进;原始数据覆盖、凭据、外部发布、破坏性操作仍遵守单独的安全边界。",
1478
1438
  "每完成一个实质步骤就检查产物、证据和计划是否一致。缺失信息若能检索、计算、重跑、改写或明确标注,应先自行修复。只有存在会改变研究问题、样本处理、操作化、估计目标、方法或解释的实质分歧时才请求研究者取舍;权限边界、不可恢复错误或当前阶段完成时停止。不得为了继续循环而虚构输入或结论。",
1479
1439
  "本次最多推进一个可验收的研究阶段。结束时简要报告已完成、已核验、阻塞、计划变化和下一步;规划内容不得表述为已有研究证据。",
1480
1440
  ].join("\n");
@@ -1581,7 +1541,7 @@ export default function psyclawExtension(pi) {
1581
1541
  repositoryRoot: arsRoot(),
1582
1542
  activeTools: toolNames,
1583
1543
  commands: commandNames,
1584
- controlledRunActive: Boolean(await readControlledRun(ctx.cwd)),
1544
+ projectActive: Boolean(await readActiveProject(ctx.cwd)),
1585
1545
  });
1586
1546
  if (typeof pi.sendMessage === "function") {
1587
1547
  pi.sendMessage({ customType: "psyclaw-ars-doctor", content: report, display: true });
@@ -1827,10 +1787,10 @@ export default function psyclawExtension(pi) {
1827
1787
  }),
1828
1788
  executionMode: "sequential",
1829
1789
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1830
- const run = await readControlledRun(ctx.cwd);
1790
+ const run = await readActiveProject(ctx.cwd);
1831
1791
  if (!run) {
1832
1792
  return {
1833
- content: [{ type: "text", text: "当前是普通对话模式,不能创建研究决策状态。请仅在用户先执行 /init、再执行 /run 后使用本工具。" }],
1793
+ content: [{ type: "text", text: "当前目录尚未 /init,不能进入研究决策状态。请先执行 /init。" }],
1834
1794
  details: { status: "general-mode" },
1835
1795
  isError: true,
1836
1796
  };
@@ -1900,9 +1860,9 @@ export default function psyclawExtension(pi) {
1900
1860
  }),
1901
1861
  executionMode: "sequential",
1902
1862
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1903
- const run = await readControlledRun(ctx.cwd);
1863
+ const run = await readActiveProject(ctx.cwd);
1904
1864
  if (!run) {
1905
- return { content: [{ type: "text", text: "当前没有有效的受控研究流程。" }], details: { status: "general-mode" }, isError: true };
1865
+ return { content: [{ type: "text", text: "当前目录尚未 /init,没有有效的研究项目。" }], details: { status: "general-mode" }, isError: true };
1906
1866
  }
1907
1867
  const eventLog = new RunEventLog(ctx.cwd, run.runId);
1908
1868
  const events = await eventLog.snapshot();
@@ -1947,9 +1907,9 @@ export default function psyclawExtension(pi) {
1947
1907
  const servers = await runtimeMcps.list(ctx.cwd, params.server);
1948
1908
  return { content: [{ type: "text", text: JSON.stringify({ servers }, null, 2) }], details: { action: "list", servers: servers.length } };
1949
1909
  }
1950
- if (!(await readControlledRun(ctx.cwd))) {
1910
+ if (!(await readActiveProject(ctx.cwd))) {
1951
1911
  return {
1952
- content: [{ type: "text", text: "当前是普通对话模式。可以查看 MCP 工具,但实际调用需先运行 /init,再运行 /run。" }],
1912
+ content: [{ type: "text", text: "当前目录尚未 /init。可以查看 MCP 工具,但实际调用需先运行 /init。" }],
1953
1913
  details: { action: "call", status: "general-mode" },
1954
1914
  isError: true,
1955
1915
  };
@@ -2080,7 +2040,7 @@ export default function psyclawExtension(pi) {
2080
2040
  label: "Research skill",
2081
2041
  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.",
2082
2042
  parameters: Type.Object({
2083
- name: Type.String({ description: "Core skill name: academic-grill, research-intake, evidence-capture, citation-audit, or research-brief" }),
2043
+ name: Type.String({ description: "Core skill name: academic-grill, research-intake, evidence-capture, citation-audit, or analysis-plan" }),
2084
2044
  purpose: Type.Optional(Type.String({ description: "Short user-facing reason for using this skill now" })),
2085
2045
  }),
2086
2046
  executionMode: "sequential",
@@ -2110,7 +2070,7 @@ export default function psyclawExtension(pi) {
2110
2070
  pi.registerTool({
2111
2071
  name: "psyclaw_workbench",
2112
2072
  label: "Research workbench",
2113
- 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.",
2073
+ 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.",
2114
2074
  parameters: Type.Object({
2115
2075
  request: Type.String({ description: "The user's research task in natural language" }),
2116
2076
  identifier: Type.Optional(Type.String({ description: "DOI, publisher URL, or exact paper title when relevant" })),
@@ -2121,11 +2081,11 @@ export default function psyclawExtension(pi) {
2121
2081
  executionMode: "sequential",
2122
2082
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
2123
2083
  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
2084
+ // Soft gate: missing /init or verify does not hard-block results.
2085
+ const project = await readActiveProject(ctx.cwd);
2086
+ const softNote = project
2127
2087
  ? undefined
2128
- : "受控 /run 未启用(软门禁):继续执行;关键字段请 AI 核查并由人 /verify。";
2088
+ : "尚未 /init(软门禁):继续执行;关键字段请 AI 核查并由人 /verify。建议先 /init 建仓。";
2129
2089
  const request = params.request.trim();
2130
2090
  const combined = `${request} ${params.identifier ?? ""}`;
2131
2091
  const explicitWorkflow = params.workflow?.trim();
@@ -2183,7 +2143,7 @@ export default function psyclawExtension(pi) {
2183
2143
  schemaVersion: "psyclaw/academic-report-plan/v1",
2184
2144
  workflow: "academic-analysis-report",
2185
2145
  status: "plan-required",
2186
- requiredSkills: ["research-intake", "evidence-capture", "citation-audit", "research-brief"],
2146
+ requiredSkills: ["research-intake", "evidence-capture", "citation-audit"],
2187
2147
  requiredDeliverables: ["analysis-plan", "source-backed-reference-list", "publication-ready-figures-or-reproducible-plot-script", "markdown-report", "docx-report"],
2188
2148
  documentExport: {
2189
2149
  primary: "pandoc",
@@ -2249,9 +2209,9 @@ export default function psyclawExtension(pi) {
2249
2209
  executionMode: "sequential",
2250
2210
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
2251
2211
  try {
2252
- if (!(await readControlledRun(ctx.cwd))) {
2212
+ if (!(await readActiveProject(ctx.cwd))) {
2253
2213
  return {
2254
- content: [{ type: "text", text: "当前是普通对话模式;请先运行 /init,再运行 /run,之后才会启用引用归档与论文证据门禁。" }],
2214
+ content: [{ type: "text", text: "当前目录尚未 /init;请先运行 /init,之后才会启用引用归档与论文证据门禁。" }],
2255
2215
  details: { status: "general-mode" },
2256
2216
  };
2257
2217
  }
@@ -2341,7 +2301,7 @@ export default function psyclawExtension(pi) {
2341
2301
  return;
2342
2302
  }
2343
2303
  // Do not let the runner create a partial `.psyclaw/runs` tree outside
2344
- // an explicitly activated `/init` -> `/run` research workflow.
2304
+ // an explicitly initialized `/init` research project.
2345
2305
  try {
2346
2306
  await readProject(ctx.cwd);
2347
2307
  }