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.
- package/README.md +12 -11
- package/agents/recommended/catalog.json +2 -2
- package/dist/src/adapters/pi/extension.js +310 -256
- package/dist/src/adapters/pi/extension.js.map +1 -1
- package/dist/src/analysis/hooks.js +1 -1
- package/dist/src/analysis/hooks.js.map +1 -1
- package/dist/src/analysis/plan.d.ts +82 -0
- package/dist/src/analysis/plan.js +246 -0
- package/dist/src/analysis/plan.js.map +1 -0
- package/dist/src/analysis/stats-router.d.ts +13 -0
- package/dist/src/analysis/stats-router.js +44 -0
- package/dist/src/analysis/stats-router.js.map +1 -0
- package/dist/src/ars/academic-router.d.ts +33 -0
- package/dist/src/ars/academic-router.js +204 -0
- package/dist/src/ars/academic-router.js.map +1 -0
- package/dist/src/ars/doctor.d.ts +27 -0
- package/dist/src/ars/doctor.js +121 -0
- package/dist/src/ars/doctor.js.map +1 -0
- package/dist/src/ars/mode-editor.d.ts +14 -7
- package/dist/src/ars/mode-editor.js +39 -23
- package/dist/src/ars/mode-editor.js.map +1 -1
- package/dist/src/ars/profile.d.ts +10 -0
- package/dist/src/ars/profile.js +15 -2
- package/dist/src/ars/profile.js.map +1 -1
- package/dist/src/branding.js +15 -16
- package/dist/src/branding.js.map +1 -1
- package/dist/src/cli.js +1 -8
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +6 -1
- package/dist/src/index.js +6 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/panel/server.js +1 -1
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/project/approvals.d.ts +4 -3
- package/dist/src/project/approvals.js +4 -3
- package/dist/src/project/approvals.js.map +1 -1
- package/dist/src/project/bootstrap.d.ts +7 -2
- package/dist/src/project/bootstrap.js +14 -13
- package/dist/src/project/bootstrap.js.map +1 -1
- package/dist/src/project/paths.d.ts +10 -0
- package/dist/src/project/paths.js +20 -3
- package/dist/src/project/paths.js.map +1 -1
- package/dist/src/project/workspace.d.ts +14 -0
- package/dist/src/project/workspace.js +82 -0
- package/dist/src/project/workspace.js.map +1 -0
- package/dist/src/session/modes.d.ts +10 -0
- package/dist/src/session/modes.js +57 -0
- package/dist/src/session/modes.js.map +1 -0
- package/dist/src/skills/recommended.js +1 -1
- package/dist/src/skills/recommended.js.map +1 -1
- package/dist/src/style/cli-ui.js +3 -3
- package/dist/src/style/cli-ui.js.map +1 -1
- package/dist/src/verify/checklist.d.ts +20 -0
- package/dist/src/verify/checklist.js +71 -0
- package/dist/src/verify/checklist.js.map +1 -0
- package/package.json +2 -1
- package/scripts/rebrand-pi.mjs +2 -2
- package/skills/core/academic-grill/SKILL.md +1 -1
- package/skills/core/analysis-plan/SKILL.md +29 -0
- package/skills/core/manifest.json +3 -3
- package/vendor/ars/pi/wrapper.js +14 -5
- package/vendor/ars/pi/wrapper.test.mjs +10 -0
- package/dist/src/research/brief.d.ts +0 -11
- package/dist/src/research/brief.js +0 -81
- package/dist/src/research/brief.js.map +0 -1
- 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,
|
|
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
|
|
44
|
-
throw new Error("Usage: /init [--paradigm survey-observational]
|
|
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", "
|
|
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
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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: "
|
|
1060
|
+
description: "Cycle chat → analysis → academic",
|
|
1145
1061
|
handler: () => {
|
|
1146
|
-
|
|
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
|
|
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
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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.
|
|
1195
|
-
|
|
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.
|
|
1198
|
-
|
|
1123
|
+
editor.onCycleMode = () => {
|
|
1124
|
+
cycleSessionMode();
|
|
1199
1125
|
};
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
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 (
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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
|
|
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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
-
|
|
1260
|
-
|
|
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
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
-
|
|
1302
|
-
|
|
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
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
|
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
|
|
1367
|
-
ctx.ui.notify("请先使用 /
|
|
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
|
|
1422
|
-
ctx.ui.notify("
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
1512
|
-
"Thinking:Ctrl+Shift+Tab
|
|
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
|
|
1534
|
+
ctx.ui.notify("ARS 与 Nature/compose 技能已内置:Shift+Tab 切到 academic。", "info");
|
|
1521
1535
|
return;
|
|
1522
1536
|
}
|
|
1523
|
-
|
|
1524
|
-
?
|
|
1525
|
-
|
|
1526
|
-
?
|
|
1527
|
-
:
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1797
|
+
const run = await readActiveProject(ctx.cwd);
|
|
1762
1798
|
if (!run) {
|
|
1763
1799
|
return {
|
|
1764
|
-
content: [{ type: "text", text: "
|
|
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
|
|
1870
|
+
const run = await readActiveProject(ctx.cwd);
|
|
1835
1871
|
if (!run) {
|
|
1836
|
-
return { content: [{ type: "text", text: "
|
|
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
|
|
1917
|
+
if (!(await readActiveProject(ctx.cwd))) {
|
|
1882
1918
|
return {
|
|
1883
|
-
content: [{ type: "text", text: "
|
|
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
|
|
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
|
|
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
|
-
|
|
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"
|
|
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
|
|
2219
|
+
if (!(await readActiveProject(ctx.cwd))) {
|
|
2166
2220
|
return {
|
|
2167
|
-
content: [{ type: "text", text: "
|
|
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
|
|
2311
|
+
// an explicitly initialized `/init` research project.
|
|
2258
2312
|
try {
|
|
2259
2313
|
await readProject(ctx.cwd);
|
|
2260
2314
|
}
|