psyclaw 0.29.21 → 0.30.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 (35) hide show
  1. package/README.md +6 -6
  2. package/dist/src/adapters/pi/extension.js +94 -28
  3. package/dist/src/adapters/pi/extension.js.map +1 -1
  4. package/dist/src/analysis/plan.d.ts +11 -0
  5. package/dist/src/analysis/plan.js +38 -1
  6. package/dist/src/analysis/plan.js.map +1 -1
  7. package/dist/src/analysis/stats-router.js +7 -4
  8. package/dist/src/analysis/stats-router.js.map +1 -1
  9. package/dist/src/ars/academic-router.js +6 -0
  10. package/dist/src/ars/academic-router.js.map +1 -1
  11. package/dist/src/branding.js +7 -7
  12. package/dist/src/branding.js.map +1 -1
  13. package/dist/src/core/citations.d.ts +3 -2
  14. package/dist/src/core/citations.js +5 -4
  15. package/dist/src/core/citations.js.map +1 -1
  16. package/dist/src/literature/archive.d.ts +3 -1
  17. package/dist/src/literature/archive.js +41 -0
  18. package/dist/src/literature/archive.js.map +1 -1
  19. package/dist/src/project/bootstrap.d.ts +12 -0
  20. package/dist/src/project/bootstrap.js +90 -2
  21. package/dist/src/project/bootstrap.js.map +1 -1
  22. package/dist/src/session/help.js +4 -4
  23. package/dist/src/session/help.js.map +1 -1
  24. package/dist/src/session/modes.js +12 -8
  25. package/dist/src/session/modes.js.map +1 -1
  26. package/dist/src/style/cli-ui.js +1 -1
  27. package/dist/src/style/cli-ui.js.map +1 -1
  28. package/dist/src/verify/checklist.d.ts +5 -0
  29. package/dist/src/verify/checklist.js +9 -1
  30. package/dist/src/verify/checklist.js.map +1 -1
  31. package/docs/PsyClaw/344/275/277/347/224/250/347/231/275/347/232/256/344/271/{246_v0.29.15.md → 246_v0.30.1.md} +80 -57
  32. package/docs//344/275/277/347/224/250/347/231/275/347/232/256/344/271/246.md +1 -1
  33. package/package.json +2 -2
  34. package/skills/core/academic-grill/SKILL.md +1 -1
  35. package/skills/core/analysis-plan/SKILL.md +12 -8
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.29.21`。正式命令、用户配置目录和后续发布统一使用 `psyclaw`。
7
+ 当前版本:`0.30.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.29.21
24
+ npm install -g psyclaw@0.30.1
25
25
  ```
26
26
 
27
27
  如果本机 npm 配置把 registry 误写成带有 `~/` 的地址,请显式指定官方源:
28
28
 
29
29
  ```bash
30
- npm install -g psyclaw@0.29.21 --registry=https://registry.npmjs.org/
30
+ npm install -g psyclaw@0.30.1 --registry=https://registry.npmjs.org/
31
31
  ```
32
32
 
33
33
  中国大陆网络较慢或无法访问官方源时:
34
34
 
35
35
  ```powershell
36
- npm install -g psyclaw@0.29.21 --registry=https://registry.npmmirror.com
36
+ npm install -g psyclaw@0.30.1 --registry=https://registry.npmmirror.com
37
37
  ```
38
38
 
39
39
  或安装脚本:
@@ -46,7 +46,7 @@ PSYCLAW_CN=1 curl -fsSL https://exekiel179.github.io/psyclaw/install.sh | sh
46
46
  ```powershell
47
47
  # Windows PowerShell
48
48
  irm https://exekiel179.github.io/psyclaw/install.ps1 | iex
49
- # 可选:$env:PSYCLAW_CN = "1";$env:PSYCLAW_VERSION = "0.29.21"
49
+ # 可选:$env:PSYCLAW_CN = "1";$env:PSYCLAW_VERSION = "0.30.1"
50
50
  ```
51
51
 
52
52
  检测到国内 npm registry(如 npmmirror)、`PSYCLAW_CN=1` 或 `PSYCLAW_GITHUB_MIRROR` 时,内置路径统一走国内可用路由:
@@ -189,7 +189,7 @@ rm -rf ~/.psyclaw
189
189
 
190
190
  ## 文档
191
191
 
192
- - [PsyClaw v0.26.0 使用白皮书](docs/PsyClaw使用白皮书_v0.26.0.md)
192
+ - [PsyClaw v0.30.1 使用白皮书](docs/PsyClaw使用白皮书_v0.30.1.md)
193
193
  - [项目范围与里程碑](docs/开工纪要.md)
194
194
  - [架构蓝图](docs/架构蓝图.md)
195
195
  - [评测框架](docs/评测框架.md)
@@ -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, assertResearchDecision, bootstrapProject, resolveResearchDecision, runInstitutionalFulltext, runLiteratureReview, runExpertReview, runAnalysisDelegation, runWritingReview, runMetaAnalysis, createStageRunner, exportAcademicDocument, recordCitationUse, runParallelPeerReview } from "../../index.js";
3
+ import { appendApproval, approvalInputDigest, assertResearchDecision, bootstrapProject, ensureProjectBinding, hasCanonicalWorkspace, resolveResearchDecision, runInstitutionalFulltext, runLiteratureReview, runExpertReview, runAnalysisDelegation, runWritingReview, runMetaAnalysis, createStageRunner, exportAcademicDocument, recordCitationUse, runParallelPeerReview } from "../../index.js";
4
4
  import { runPlanWithPi } from "../../orchestration/pi-executor.js";
5
5
  import { formatEffects, hasElevatedEffects, normalizeEffects, toolsForEffects } from "../../orchestration/effects.js";
6
6
  import { agentManagerRows } from "../../agents/recommended-personas.js";
@@ -32,7 +32,7 @@ import { arsRoot } from "../../ars/pi-panel-executor.js";
32
32
  import { ArsModeEditor, isArsModeEditorText } from "../../ars/mode-editor.js";
33
33
  import { MODE_STATUS, detectChatModeMismatch, formatChatModeMismatchNotice, nextSessionMode, parseSessionMode, sessionModePrompt, } from "../../session/modes.js";
34
34
  import { continuouslyWorkWarningText, isContinuouslyWorkEnabled, } from "../../session/continuously-work.js";
35
- import { assertHumanVerifyGate, formatVerifyChecklist, isNaturalPlanConfirm, ensureDefaultVerifyChecklist, loadVerifyChecklist } from "../../verify/checklist.js";
35
+ import { assertHumanVerifyGate, formatVerifyChecklist, isNaturalPlanConfirm, isPlanRitualConfirm, PLAN_RITUAL_PHRASE, ensureDefaultVerifyChecklist, loadVerifyChecklist } from "../../verify/checklist.js";
36
36
  import { formatSessionHelp, formatSessionHelpBrief } from "../../session/help.js";
37
37
  import { openResearchWorkbench } from "../../panel/workbench.js";
38
38
  import { captureAgentError, initNodeObservability, readTelemetryPreference, shutdownObservability, trackGateWaiting, writeTelemetryPreference, } from "../../observability/index.js";
@@ -181,10 +181,10 @@ function academicGrillRequest(subject, mode) {
181
181
  : "持久化模式:review。追问完成后先展示拟更新摘要和受影响文件,并询问我是否写回;得到明确确认前不得修改项目文档。",
182
182
  ].join("\n");
183
183
  }
184
- /** Lightweight ideation path never routes through academic-grill / /grill. */
184
+ /** Lightweight ideation path for research directions. */
185
185
  function academicBrainstormRequest(subject) {
186
186
  return [
187
- "这是 /brainstorm,不是 /grill:不要加载 academic-grill Skill,不要进入逐题压力测试或研究规格访谈。",
187
+ "这是 /brainstorm:做选题向头脑风暴。不要加载 academic-grill,不要进入逐题压力测试或研究规格访谈。",
188
188
  subject
189
189
  ? `头脑风暴主题:${subject}`
190
190
  : "根据当前对话与项目材料确定头脑风暴主题;已有信息不要重复询问。",
@@ -193,12 +193,12 @@ function academicBrainstormRequest(subject) {
193
193
  "2. 简要比较各自的价值、可行性、数据需求与推断边界。",
194
194
  "3. 明确推荐其中一个,并说明推荐理由与主要取舍。",
195
195
  "4. 最多再问 1-2 个真正影响选题的澄清问题(可一次给出);不要展开完整的设计/测量/估计量审讯。",
196
- "不要执行分析、不要写论文、不要伪造文献或结果。若用户接下来需要严格压力测试,请提示他们另用 /grill。",
196
+ "不要执行分析、不要写论文、不要伪造文献或结果。",
197
197
  ].join("\n");
198
198
  }
199
199
  async function readActiveProject(root) {
200
200
  try {
201
- const project = await readProject(root);
201
+ const { project } = await ensureProjectBinding({ root });
202
202
  return {
203
203
  projectId: project.id,
204
204
  runId: `project_${project.id}`,
@@ -1049,29 +1049,72 @@ export default function psyclawExtension(pi) {
1049
1049
  };
1050
1050
  }
1051
1051
  }
1052
- // Natural-language plan confirm: reply「可以」instead of forcing /plan confirm.
1053
- if (mode === "analysis" && isNaturalPlanConfirm(trimmed)) {
1052
+ // Plan confirmations: soft「可以」= per-node / method acknowledge; ritual phrase = authorize execute.
1053
+ if (mode === "analysis" && (isPlanRitualConfirm(trimmed) || isNaturalPlanConfirm(trimmed))) {
1054
1054
  const plan = await readActiveAnalysisPlan(ctx.cwd);
1055
1055
  if (plan && (plan.status === "awaiting-confirm" || plan.status === "ready" || plan.status === "reviewing")) {
1056
1056
  const method = plan.confirmedMethod || plan.primaryAnalysis || plan.proposedMethods[0] || "按当前方案执行";
1057
+ const pendingChoices = (plan.analysisChoices ?? []).filter((row) => !row.selected);
1058
+ const ritual = isPlanRitualConfirm(trimmed);
1059
+ if (!ritual) {
1060
+ // Soft acknowledge only — never jump to run-now.
1061
+ let next = plan.status === "awaiting-confirm" && !plan.confirmedMethod
1062
+ ? advanceAnalysisPlan(plan, { type: "confirm", method, backend: "local-script" })
1063
+ : plan;
1064
+ next = await writeAnalysisPlan(ctx.cwd, next);
1065
+ const needChoices = pendingChoices.length > 0
1066
+ ? `仍有 ${pendingChoices.length} 项子分析待选;请先逐项确认。`
1067
+ : `方法已记下。要开始执行,请完整输入:「${PLAN_RITUAL_PHRASE}」`;
1068
+ ctx.ui.notify(`${formatAnalysisPlanStatus(next)}\n${needChoices}`, "info");
1069
+ return {
1070
+ action: "transform",
1071
+ text: [
1072
+ "/skill:analysis-plan",
1073
+ "",
1074
+ `User soft-acknowledged ("${trimmed}"). Do NOT run scripts yet.`,
1075
+ `Confirmed method so far: ${next.confirmedMethod ?? method}`,
1076
+ pendingChoices.length > 0
1077
+ ? `Pending per-analysis choices: ${pendingChoices.map((row) => row.id).join(", ")}. Present the next undecided choice card and wait.`
1078
+ : `All listed choices are decided (or none yet). Ask the user to type exactly: ${PLAN_RITUAL_PHRASE}`,
1079
+ "Do not start /crosscheck or /verify until the user explicitly agrees to open that block.",
1080
+ ].join("\n"),
1081
+ };
1082
+ }
1083
+ if (pendingChoices.length > 0) {
1084
+ ctx.ui.notify(`${formatAnalysisPlanStatus(plan)}\n仪式句已收到,但还有 ${pendingChoices.length} 项子分析未确认,请先逐项选完。`, "warning");
1085
+ return {
1086
+ action: "transform",
1087
+ text: [
1088
+ "/skill:analysis-plan",
1089
+ "",
1090
+ `User typed the ritual phrase but ${pendingChoices.length} analysis choice(s) remain undecided.`,
1091
+ "Present the next undecided choice card and wait. Do not execute.",
1092
+ ].join("\n"),
1093
+ };
1094
+ }
1057
1095
  let next = plan.status === "awaiting-confirm"
1058
1096
  ? advanceAnalysisPlan(plan, { type: "confirm", method, backend: "local-script" })
1059
1097
  : plan;
1098
+ next = advanceAnalysisPlan(next, { type: "ritual-approve", text: PLAN_RITUAL_PHRASE });
1060
1099
  if (next.status === "reviewing" || next.status === "awaiting-confirm") {
1061
1100
  next = advanceAnalysisPlan(next, { type: "review" });
1062
1101
  }
1063
1102
  next = advanceAnalysisPlan(next, { type: "run-now" });
1064
1103
  next = await writeAnalysisPlan(ctx.cwd, next);
1065
- ctx.ui.notify(`${formatAnalysisPlanStatus(next)}\n已按自然语言确认并开始执行。`, "info");
1104
+ if (next.status !== "running") {
1105
+ ctx.ui.notify(`${formatAnalysisPlanStatus(next)}\n未能进入执行(检查子项确认与仪式批准)。`, "warning");
1106
+ return { action: "handled" };
1107
+ }
1108
+ ctx.ui.notify(`${formatAnalysisPlanStatus(next)}\n已记录仪式批准并开始执行。`, "info");
1066
1109
  return {
1067
1110
  action: "transform",
1068
1111
  text: [
1069
1112
  "/skill:analysis-plan",
1070
1113
  "",
1071
- `User confirmed with natural language ("${trimmed}"). Execute plan ${next.id} now.`,
1114
+ `User completed plan ritual ("${PLAN_RITUAL_PHRASE}"). Execute plan ${next.id} now.`,
1072
1115
  `Confirmed method: ${next.confirmedMethod ?? method}`,
1073
- next.approvalMode === "auto" ? "approvalMode=auto: label outputs as 未经人审批." : "Human confirmed this step.",
1074
- "Before and after analysis, open Panel checklist or /crosscheck; skipping must mark 未经核对.",
1116
+ next.approvalMode === "auto" ? "approvalMode=auto: label outputs as 未经人审批." : "Human ritual approval recorded.",
1117
+ "Do NOT auto-start /crosscheck or /verify. After scripts finish, ask whether to open those blocks; only proceed if the user agrees.",
1075
1118
  ].join("\n"),
1076
1119
  };
1077
1120
  }
@@ -1079,7 +1122,11 @@ export default function psyclawExtension(pi) {
1079
1122
  const wantsStats = mode === "analysis" && Boolean(resolveStatsIntent(trimmed));
1080
1123
  const wantsAcademic = mode === "academic" && Boolean(resolveAcademicSoftRoute(trimmed));
1081
1124
  if ((wantsStats || wantsAcademic) && !(await readActiveProject(ctx.cwd))) {
1082
- if (!ctx.hasUI) {
1125
+ if (await hasCanonicalWorkspace(ctx.cwd)) {
1126
+ // Heal binding silently — prior /init layout already present.
1127
+ await ensureProjectBinding({ root: ctx.cwd }).catch(() => undefined);
1128
+ }
1129
+ else if (!ctx.hasUI) {
1083
1130
  return {
1084
1131
  action: "transform",
1085
1132
  text: [
@@ -1089,13 +1136,15 @@ export default function psyclawExtension(pi) {
1089
1136
  ].join("\n"),
1090
1137
  };
1091
1138
  }
1092
- const choice = await ctx.ui.select("尚未 /init。统计/写作建议先建仓,否则交接与核对清单不完整。", ["先执行 /init", "本次继续(稍后 init)", "取消"], { timeout: 120_000 });
1093
- if (choice === "取消" || choice === undefined)
1094
- return { action: "handled" };
1095
- if (choice === "先执行 /init") {
1096
- return { action: "transform", text: `/init\n\n(用户选择先初始化,完成后再继续:${trimmed})` };
1139
+ else {
1140
+ const choice = await ctx.ui.select("尚未 /init。统计/写作建议先建仓,否则交接与核对清单不完整。", ["先执行 /init", "本次继续(稍后 init)", "取消"], { timeout: 120_000 });
1141
+ if (choice === "取消" || choice === undefined)
1142
+ return { action: "handled" };
1143
+ if (choice === "先执行 /init") {
1144
+ return { action: "transform", text: `/init\n\n(用户选择先初始化,完成后再继续:${trimmed})` };
1145
+ }
1146
+ ctx.ui.notify("继续本次请求;建议随后 /init。关键结果请做交叉核验。", "warning");
1097
1147
  }
1098
- ctx.ui.notify("继续本次请求;建议随后 /init。关键结果请做交叉核验。", "warning");
1099
1148
  }
1100
1149
  if (mode === "academic") {
1101
1150
  const match = resolveAcademicSoftRoute(event.text);
@@ -1156,7 +1205,17 @@ export default function psyclawExtension(pi) {
1156
1205
  handler: async (args, ctx) => {
1157
1206
  try {
1158
1207
  const parsed = parseInitArgs(args);
1208
+ const already = await hasCanonicalWorkspace(ctx.cwd);
1209
+ const hadProject = Boolean(await readActiveProject(ctx.cwd).catch(() => null));
1159
1210
  const project = await bootstrapProject({ root: ctx.cwd, ...parsed });
1211
+ if (already || hadProject) {
1212
+ ctx.ui.notify([
1213
+ `已检测到既有工作区,无需重新建仓:${project.id}`,
1214
+ "规范目录与 psyclaw.md 已存在;跨会话可直接继续 analysis / academic。",
1215
+ "Shift+Tab:chat → analysis → academic · 输入 /help 查看速览。",
1216
+ ].join("\n"), "info");
1217
+ return;
1218
+ }
1160
1219
  ctx.ui.notify([
1161
1220
  `工作仓库已初始化:${project.id}`,
1162
1221
  "已创建 data/raw|clean、analysis/、literature/、paper/、psyclaw.md、.psyclaw/",
@@ -1191,9 +1250,9 @@ export default function psyclawExtension(pi) {
1191
1250
  "3) 格式与报告要求:APA/三线表/必备报告字段、清单项是否齐;",
1192
1251
  "4) 过程产物:脚本入口、中间文件、可复现痕迹是否齐全。",
1193
1252
  "执行方式:派出至少两个独立审查视角(例如:数据一致性;引文存在性;格式/报告合同),各自给出发现后再合并为一份报告;标出一致项与冲突项。",
1194
- "不得自称已过人审。人审由系统在交接/定稿门禁自动要求,经 Panel「核实」或唤醒选项完成。",
1253
+ "不得自称已过人审。不要在用户未明确同意前自动启动本核对。人审由系统在交接/定稿门禁自动要求,经 Panel「核实」或唤醒选项完成。",
1195
1254
  ].join("\n"), ctx.isIdle() ? {} : { deliverAs: "followUp" });
1196
- ctx.ui.notify(`已启动过程性 /crosscheck(焦点:${focus})。人审由收尾门禁自动要求。`, "info");
1255
+ ctx.ui.notify(`已启动过程性 /crosscheck(焦点:${focus})。大段核对应先征得用户同意。`, "info");
1197
1256
  }
1198
1257
  catch (error) {
1199
1258
  await notifyError(ctx, error);
@@ -1320,11 +1379,11 @@ export default function psyclawExtension(pi) {
1320
1379
  });
1321
1380
  if (!legacyTestApi)
1322
1381
  pi.registerCommand("brainstorm", {
1323
- description: "研究方向头脑风暴:提出并比较可选问题,不做压力测试",
1382
+ description: "研究方向头脑风暴:提出并比较可选问题",
1324
1383
  handler: async (args, ctx) => {
1325
1384
  const subject = args.trim();
1326
1385
  pi.sendUserMessage(academicBrainstormRequest(subject), ctx.isIdle() ? {} : { deliverAs: "followUp" });
1327
- ctx.ui.notify("已启动研究方向头脑风暴(不经过 /grill)。若要严格压力测试,请另开 /grill。", "info");
1386
+ ctx.ui.notify("已启动研究方向头脑风暴。", "info");
1328
1387
  },
1329
1388
  });
1330
1389
  if (!legacyTestApi)
@@ -2272,6 +2331,7 @@ export default function psyclawExtension(pi) {
2272
2331
  reason: Type.String({ description: "One-line reason this source is cited at this position (e.g. 支持:社会支持缓冲压力假说)" }),
2273
2332
  context: Type.String({ description: "The sentence/context in the manuscript where the citation appears" }),
2274
2333
  section: Type.Optional(Type.String({ description: "Manuscript section, e.g. 1 引言" })),
2334
+ downloadOa: Type.Optional(Type.Boolean({ description: "Set true only after the researcher explicitly approved downloading the open-access PDF" })),
2275
2335
  }),
2276
2336
  executionMode: "sequential",
2277
2337
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -2287,8 +2347,18 @@ export default function psyclawExtension(pi) {
2287
2347
  reason: params.reason,
2288
2348
  context: params.context,
2289
2349
  ...(params.section === undefined || !params.section.trim() ? {} : { section: params.section }),
2350
+ ...(params.downloadOa === true ? { downloadOa: true } : {}),
2290
2351
  });
2291
2352
  const ref = result.reference;
2353
+ const fulltextNext = result.record.verified && result.fulltext?.status === "downloaded"
2354
+ ? `引用已双源核验,开放全文已保存到 ${result.fulltext.localPath}。`
2355
+ : result.fulltext?.status === "approval-required"
2356
+ ? `引用已双源核验,发现开放全文候选。请先征得用户同意,再以 downloadOa=true 重新调用本工具下载到 ${result.fulltext.localPath}(候选:${result.fulltext.sourceUrl ?? "OA URL"})。`
2357
+ : result.fulltext?.status === "manual-download-required"
2358
+ ? `该引用的题录已登记,但未找到开放 PDF。请通过 ${result.fulltext.doiUrl} 使用你的合法访问权限下载,并保存到 ${result.fulltext.localPath};完成前不得将依赖全文的主张写入正文。`
2359
+ : result.fulltext?.status === "download-failed"
2360
+ ? `开放全文下载失败:${result.fulltext.reason ?? "未知原因"}。请通过 ${result.fulltext.doiUrl} 自行合法获取并保存到 ${result.fulltext.localPath}。`
2361
+ : "该 DOI 或全文未能完成核验;请在最终交付前补核验或替换来源。";
2292
2362
  return {
2293
2363
  content: [{ type: "text", text: JSON.stringify({
2294
2364
  schemaVersion: "psyclaw/citation-use/v1",
@@ -2299,11 +2369,7 @@ export default function psyclawExtension(pi) {
2299
2369
  referenceTitle: ref?.title ?? null,
2300
2370
  archived: ref !== null,
2301
2371
  fulltext: result.fulltext,
2302
- next: result.record.verified && result.fulltext?.status === "downloaded"
2303
- ? `引用已双源核验,开放全文已保存到 ${result.fulltext.localPath}。`
2304
- : result.fulltext?.status === "manual-download-required"
2305
- ? `该引用的题录已登记,但未找到开放 PDF。请通过 ${result.fulltext.doiUrl} 使用你的合法访问权限下载,并保存到 ${result.fulltext.localPath};完成前不得将依赖全文的主张写入正文。`
2306
- : "该 DOI 或全文未能完成核验;请在最终交付前补核验或替换来源。",
2372
+ next: fulltextNext,
2307
2373
  }, null, 2) }],
2308
2374
  details: { citationId: result.record.citationId, doi: result.record.doi, verified: result.record.verified },
2309
2375
  };