psyclaw 0.27.15 → 0.27.18
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 +6 -7
- package/dist/apps/panel/index.html +8 -7
- package/dist/src/adapters/pi/extension.js +424 -175
- package/dist/src/adapters/pi/extension.js.map +1 -1
- package/dist/src/analysis/hooks.js +3 -3
- package/dist/src/analysis/hooks.js.map +1 -1
- package/dist/src/analysis/pre-registration.d.ts +5 -4
- package/dist/src/analysis/pre-registration.js +3 -3
- package/dist/src/analysis/pre-registration.js.map +1 -1
- package/dist/src/branding.d.ts +1 -1
- package/dist/src/branding.js +17 -12
- package/dist/src/branding.js.map +1 -1
- package/dist/src/chat.js +9 -0
- package/dist/src/chat.js.map +1 -1
- package/dist/src/cli.js +1 -18
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/orchestration/contracts.d.ts +4 -1
- package/dist/src/panel/events.js +28 -1
- package/dist/src/panel/events.js.map +1 -1
- package/dist/src/panel/extension.d.ts +1 -1
- package/dist/src/panel/extension.js +16 -9
- package/dist/src/panel/extension.js.map +1 -1
- package/dist/src/panel/projection.d.ts +1 -1
- package/dist/src/panel/projection.js +30 -12
- package/dist/src/panel/projection.js.map +1 -1
- package/dist/src/panel/server.d.ts +11 -1
- package/dist/src/panel/server.js +86 -4
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/project/approvals.d.ts +8 -0
- package/dist/src/project/approvals.js +6 -7
- package/dist/src/project/approvals.js.map +1 -1
- package/dist/src/project/bootstrap.d.ts +0 -17
- package/dist/src/project/bootstrap.js +1 -87
- package/dist/src/project/bootstrap.js.map +1 -1
- package/dist/src/project/hitl.d.ts +4 -1
- package/dist/src/project/hitl.js +8 -5
- package/dist/src/project/hitl.js.map +1 -1
- package/dist/src/research/brief.js +1 -1
- package/dist/src/research/brief.js.map +1 -1
- package/dist/src/research/decision.d.ts +46 -0
- package/dist/src/research/decision.js +85 -0
- package/dist/src/research/decision.js.map +1 -0
- package/dist/src/style/cli-ui.js +2 -3
- package/dist/src/style/cli-ui.js.map +1 -1
- package/dist/src/tui/skill-manager.d.ts +1 -0
- package/dist/src/tui/skill-manager.js +6 -1
- package/dist/src/tui/skill-manager.js.map +1 -1
- package/dist/src/wizard.js +16 -7
- package/dist/src/wizard.js.map +1 -1
- package/dist/src/workflows/expert-review-runner.js +4 -6
- package/dist/src/workflows/expert-review-runner.js.map +1 -1
- package/dist/src/workflows/institutional-fulltext.js +3 -3
- package/dist/src/workflows/institutional-fulltext.js.map +1 -1
- package/dist/src/workflows/publish.js +3 -2
- package/dist/src/workflows/publish.js.map +1 -1
- package/dist/src/workflows/spec.js +1 -1
- package/dist/src/workflows/spec.js.map +1 -1
- package/dist/src/workflows/writing-review.js +40 -7
- package/dist/src/workflows/writing-review.js.map +1 -1
- package/package.json +3 -2
- package/scripts/psyclaw.mjs +27 -0
- package/scripts/rebrand-pi.mjs +77 -16
- package/skills/core/academic-grill/SKILL.md +9 -1
- package/skills/core/citation-audit/SKILL.md +2 -0
- package/skills/core/research-brief/SKILL.md +6 -0
- package/skills/core/research-intake/SKILL.md +1 -1
- package/skills/recommended/catalog.json +11 -28
- package/skills/recommended/mcp-catalog.json +13 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { DefaultPackageManager, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { appendApproval, approvalInputDigest,
|
|
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";
|
|
4
4
|
import { runPlanWithPi } from "../../orchestration/pi-executor.js";
|
|
5
5
|
import { atomicWriteFile } from "../../project/jsonl.js";
|
|
6
6
|
import { RunEventLog } from "../../panel/events.js";
|
|
@@ -56,8 +56,15 @@ async function runPluginCommand(args) {
|
|
|
56
56
|
throw new Error(`不支持的 Plugin 操作:${args[0] ?? ""}`);
|
|
57
57
|
}
|
|
58
58
|
const activeAgentRuns = new Set();
|
|
59
|
-
const activeApprovalDialogs = new Set();
|
|
60
59
|
const CORE_SKILLS = new Set(["academic-grill", "research-intake", "evidence-capture", "citation-audit", "research-brief"]);
|
|
60
|
+
function findLastMatching(items, predicate) {
|
|
61
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
62
|
+
const item = items[index];
|
|
63
|
+
if (item !== undefined && predicate(item))
|
|
64
|
+
return item;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
61
68
|
function coreSkillPath(name) {
|
|
62
69
|
return join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..", "skills", "core", name, "SKILL.md");
|
|
63
70
|
}
|
|
@@ -166,10 +173,11 @@ function academicGrillRequest(subject, mode) {
|
|
|
166
173
|
subject
|
|
167
174
|
? `本次要讨论的研究主题或方案:${subject}`
|
|
168
175
|
: "从当前对话、当前项目文件和已有研究状态中确定要讨论的研究主题或方案;能从这些材料查到的信息不要反问我。",
|
|
169
|
-
"
|
|
170
|
-
"
|
|
176
|
+
"先根据研究主题、数据字段和已有材料提出 2-4 个有理论意义、可由现有材料回答的研究问题或假设,简述价值、可行性和边界,并推荐一个;不要把本应由你完成的研究构思全部反问给用户。",
|
|
177
|
+
"随后从最上游、影响最大的未决问题开始。只有真正影响分析或解释且无法从现有材料判断时才提问;每轮只问一个问题,同时给出你的推荐答案或推荐决策及主要取舍。",
|
|
178
|
+
"明确区分探索性与确证性研究。用户直接提供数据并要求分析时,默认按探索性、假设生成性工作处理,除非用户明确说明方案在看数据前已经确定。只有明确的确证性研究才询问哪些内容事先确定;不要求提供预注册链接。不把规划内容当作已有证据。直到关键分支已经解决或明确交由研究者决定后,再整理研究规格。",
|
|
171
179
|
mode === "init"
|
|
172
|
-
? "持久化模式:init。追问完成后直接更新 academic-grill Skill
|
|
180
|
+
? "持久化模式:init。追问完成后直接更新 academic-grill Skill 规定的全部项目文档,不再询问是否更新;写入后列出实际更新的文件。能依据研究目标、数据和通行方法明确处理的内容由你提出方案并写入,状态标为 ready-for-run。只有存在两个以上均合理、且会实质改变研究问题、样本处理、估计目标、方法或结果解释的方案时,才把该项写入 notes/decision_request.md 并请求研究者取舍。"
|
|
173
181
|
: "持久化模式:review。追问完成后先展示拟更新摘要和受影响文件,并询问我是否写回;得到明确确认前不得修改项目文档。",
|
|
174
182
|
].join("\n");
|
|
175
183
|
}
|
|
@@ -179,9 +187,15 @@ async function controlledRunPath(root) {
|
|
|
179
187
|
async function readControlledRun(root) {
|
|
180
188
|
try {
|
|
181
189
|
const value = JSON.parse(await readFile(await controlledRunPath(root), "utf8"));
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
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"))
|
|
191
|
+
return null;
|
|
192
|
+
const project = await readProject(root);
|
|
193
|
+
if (project.id !== value.projectId)
|
|
194
|
+
return null;
|
|
195
|
+
const authorization = findLastMatching(await readApprovals(root), (record) => record.kind === "run-mode" && record.runId === value.runId);
|
|
196
|
+
if (authorization?.decision !== "approved" || authorization.actor !== "human")
|
|
197
|
+
return null;
|
|
198
|
+
return { ...value, mode: value.mode === "auto" ? "auto" : "human", selectedSkills: Array.isArray(value.selectedSkills) ? value.selectedSkills.filter((item) => typeof item === "string") : [] };
|
|
185
199
|
}
|
|
186
200
|
catch {
|
|
187
201
|
return null;
|
|
@@ -203,20 +217,26 @@ async function activateControlledRun(root, runId, projectId, objective, selected
|
|
|
203
217
|
}
|
|
204
218
|
function controlledRunRequest(objective, selectedSkills, mode) {
|
|
205
219
|
return [
|
|
206
|
-
`PsyClaw
|
|
220
|
+
`PsyClaw 受控研究流程已由用户通过 /run 明确启动${mode === "auto" ? ",本次采用连续执行方式" : ""}。先读取 .psyclaw/project.json、.psyclaw/controlled-run.json、.psyclaw/approvals.jsonl、notes/research-spec.md 和 notes/plan.md。`,
|
|
207
221
|
`本次目标:${objective}`,
|
|
208
222
|
selectedSkills.length > 0
|
|
209
223
|
? `本次运行由用户选择的优化 Skill:${selectedSkills.join(", ")}。在相关任务中优先加载并遵循这些 Skill;同名 Skill 只加载一次。`
|
|
210
224
|
: "本次运行未指定额外 Skill,使用 PsyClaw 默认研究流程。不要删除、停用或改写已安装的其他 Skill。",
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
225
|
+
"计划内的文件创建、脚本执行、分析委托、可恢复下载和普通工具调用连续推进,不要逐项向用户索要许可;/run 已代表用户同意这些为完成本次目标所必需且符合项目边界的操作,系统仍须记录结构化回执。凭据处理、原始数据覆盖、破坏性命令、绕过访问控制和外部发布不在该授权范围内。",
|
|
226
|
+
"人工决策只用于真正的研究取舍:存在两个以上有实质依据的合理方案,现有研究问题、数据、证据和方法规范不能确定其中之一,且不同选择会改变研究问题、样本纳入或排除、变量操作化、估计目标、分析方法或结果解释。提出决策前必须先核对可用证据与通行方法;提出时说明仍无法消解的原因、备选方案、各自依据与影响,并给出系统推荐;一次只请求一个决定。",
|
|
227
|
+
"满足上述全部条件时,先调用 psyclaw_research_decision 写入结构化研究取舍事件,再向研究者提问。该工具拒绝的事项不得改用自然语言伪装成 awaiting-human;应继续自动修复、明确报告技术限制,或按独立安全授权边界处理。",
|
|
228
|
+
"不要因软件缺失、安装配置、格式问题、脚本错误、字段可推断、统计报告遗漏、效应量或置信区间缺失、缺失值说明不足、引用格式错误或可补充的证据不足而进入人工决策。先自行安装或修复、重跑、补算、补写、检索核验或缩小表述;确实无法完成时报告具体未完成项和下一步,不把技术失败包装成研究者取舍。",
|
|
214
229
|
"实际推进当前尚未完成的研究任务;先形成或更新符合学术规范的 Markdown 分析报告,不能只描述计划。分析报告必须清楚区分数据来源、统计结果、文献证据、限制与尚未核验内容。",
|
|
230
|
+
"写作时把内部状态和校验术语留在 .psyclaw 记录中,不要把 Claim、Evidence、ledger、gate、audit、blocked、receipt 等标签直接写进面向研究者的对话、摘要或论文。中文正文避免生硬使用“审计”“结局”“依赖”“受阻”等词:分别按语境改为质量检查、结果变量或指标、所需软件或前提、无法分析。",
|
|
231
|
+
"摘要首先服从用户提供的会议或期刊格式;没有明确要求时写成一段式摘要,完整交代目的、方法、主要结果和结论,但不机械添加四个小标题。摘要只保留最关键结果,不罗列损坏列、逐列缺失数、文件哈希或实现细节。数据质量细节放在方法、附录或单独质量报告中。",
|
|
232
|
+
"若用户只提供数据集,先结合变量、研究背景和可识别的理论机制主动提出 2-4 个有研究价值且可检验的问题或假设,再推荐分析主线。不要把“是否有预注册”作为第一个或默认问题;探索性分析明确标注即可,只有用户声明为确证性研究时才核对事前分析方案,且不索要公开链接。",
|
|
233
|
+
"数据与隐私默认最小披露:对话、报告和论文先给汇总缺失率、受影响的变量组及其分析影响,不输出原始行、个体文本、标识符、完整列清单或冗长的逐列缺失信息;确需列名时只列与当前方法决策直接相关者,完整诊断另存补充材料。",
|
|
234
|
+
"参考文献按目标格式生成并逐项核对。APA 7 稿件必须做到正文引文与文末条目一一对应,区分期刊论文、会议论文、数据集、预印本和软件,核对作者顺序、年份、标题、刊名或会议名、卷期页码或文章号、DOI、标点、排序及悬挂缩进;缺少字段时不得猜测。",
|
|
215
235
|
"分析报告完成后,下一步不是直接询问是否导出 DOCX,而是询问用户是否据此撰写论文。",
|
|
216
236
|
"若用户选择撰写论文,再单独询问是否先进行文献调研,并说明该阶段可能花费较长时间;提示用户可以指定已启用的 Skill,或者选择 PsyClaw 默认的文献调研方式。不要在获得答复前自动开始长时调研。",
|
|
217
237
|
"文献调研完成并核验后,再询问是否进行全文撰写,并询问使用哪个写作 Skill;未指定时可以建议默认方案,但仍需用户确认。只有充分证据经过核验后,才能把相关主张写入论文正文。",
|
|
218
238
|
"同行评审由用户需要时另行运行 /review;不要在本轮自动审稿。/review 会再次询问使用哪个评审 Skill。最后才询问是否导出 DOCX,以及格式要求。",
|
|
219
|
-
"开放获取 PDF 自动保存到 literature/pdfs/。不得绕过付费墙;没有合法开放全文时给出 https://doi.org/<DOI> 可点击链接,提示用户通过自己的权限下载到系统给出的目标路径。论文中的每篇引用都必须完成 DOI 核验并有本地 PDF
|
|
239
|
+
"开放获取 PDF 自动保存到 literature/pdfs/。不得绕过付费墙;没有合法开放全文时给出 https://doi.org/<DOI> 可点击链接,提示用户通过自己的权限下载到系统给出的目标路径。论文中的每篇引用都必须完成 DOI 核验并有本地 PDF,否则用自然语言列出待补材料,并说明补齐前不宜定稿;不要向用户输出 blocked、gate 等内部状态标签。",
|
|
220
240
|
"如果用户在未完成文献调研、全文写作或评审前要求导出 DOCX,允许导出当前分析报告,但必须明确标为遵循学术规范的分析报告,不得称为论文。",
|
|
221
241
|
].join("\n");
|
|
222
242
|
}
|
|
@@ -235,58 +255,6 @@ function parseRunArgs(args) {
|
|
|
235
255
|
mode: auto ? "auto" : "human",
|
|
236
256
|
};
|
|
237
257
|
}
|
|
238
|
-
async function planDocumentsReady(root) {
|
|
239
|
-
try {
|
|
240
|
-
const [spec, plan] = await Promise.all([
|
|
241
|
-
readFile(join(root, "notes", "research-spec.md"), "utf8"),
|
|
242
|
-
readFile(join(root, "notes", "plan.md"), "utf8"),
|
|
243
|
-
]);
|
|
244
|
-
return /Status:\s*awaiting-human-approval/i.test(spec) && /Status:\s*awaiting-human-approval/i.test(plan);
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
async function reviewPrimaryDocuments(ctx) {
|
|
251
|
-
if (!ctx.hasUI || activeApprovalDialogs.has(ctx.cwd))
|
|
252
|
-
return false;
|
|
253
|
-
if (!(await planDocumentsReady(ctx.cwd))) {
|
|
254
|
-
ctx.ui.notify("主要计划文档尚未生成完成;请先完成 /init 的学术追问。", "warning");
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
activeApprovalDialogs.add(ctx.cwd);
|
|
258
|
-
try {
|
|
259
|
-
for (const document of PRIMARY_PLAN_DOCUMENTS) {
|
|
260
|
-
const choice = await ctx.ui.select(`审批${document.title}:${document.path}`, ["批准当前版本", "拒绝并返回修改"], { timeout: 120_000 });
|
|
261
|
-
const approved = choice === "批准当前版本";
|
|
262
|
-
await approvePrimaryDocument(ctx.cwd, document, approved ? "approved" : "rejected");
|
|
263
|
-
if (!approved) {
|
|
264
|
-
ctx.ui.notify(`${document.title}未批准;修改后运行 /approve 重新审批。`, "warning");
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
ctx.ui.notify("主要计划文档已批准。文档内容变化后审批会自动失效。", "info");
|
|
269
|
-
return true;
|
|
270
|
-
}
|
|
271
|
-
finally {
|
|
272
|
-
activeApprovalDialogs.delete(ctx.cwd);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
async function approveRunNodes(ctx, runId, mode) {
|
|
276
|
-
for (const node of DEFAULT_RUN_APPROVAL_NODES) {
|
|
277
|
-
if (mode === "auto") {
|
|
278
|
-
await appendApproval(ctx.cwd, { kind: "step", nodeId: node.id, decision: "auto-approved", actor: "auto", runId, summary: node.summary });
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
const options = node.required ? ["批准", "拒绝并停止"] : ["批准", "本次跳过", "拒绝并停止"];
|
|
282
|
-
const choice = await ctx.ui.select(`${node.title}\n${node.summary}`, options, { timeout: 120_000 });
|
|
283
|
-
const decision = choice === "批准" ? "approved" : choice === "本次跳过" ? "skipped" : "rejected";
|
|
284
|
-
await appendApproval(ctx.cwd, { kind: "step", nodeId: node.id, decision, actor: "human", runId, summary: node.summary });
|
|
285
|
-
if (decision === "rejected" || (node.required && decision !== "approved"))
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
return true;
|
|
289
|
-
}
|
|
290
258
|
function toolApprovalSummary(toolName, input) {
|
|
291
259
|
const record = input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
292
260
|
const target = [record.path, record.file_path, record.command, record.action, record.server, record.tool]
|
|
@@ -301,13 +269,18 @@ function toolNeedsApproval(toolName, input) {
|
|
|
301
269
|
}
|
|
302
270
|
function autoApprovalBlocked(toolName, input) {
|
|
303
271
|
const text = JSON.stringify(input ?? {}).toLocaleLowerCase();
|
|
304
|
-
|
|
305
|
-
|
|
272
|
+
const mutatingTool = ["write", "edit", "bash", "powershell"].includes(toolName);
|
|
273
|
+
if (mutatingTool && /(?:data[\\/]raw|credential|secret|auth\.json|api[_ -]?key)/.test(text))
|
|
274
|
+
return "该操作可能覆盖原始数据或接触凭据,不能纳入 /run 的常规授权";
|
|
306
275
|
if (["bash", "powershell"].includes(toolName) && /\b(rm|rmdir|del|remove-item|format|shutdown|reboot|git\s+reset\s+--hard)\b/i.test(text)) {
|
|
307
|
-
return "
|
|
276
|
+
return "破坏性命令不能纳入 /run 的常规授权";
|
|
308
277
|
}
|
|
309
278
|
return undefined;
|
|
310
279
|
}
|
|
280
|
+
function requiresSeparateOperationConfirmation(toolName, input) {
|
|
281
|
+
const text = `${toolName} ${JSON.stringify(input ?? {})}`;
|
|
282
|
+
return /submit\s+(?:manuscript|paper|form)|投稿(?:到|至)|发布到(?:外部|期刊|会议|网站)|send\s+(?:email|message)|邮件发送/i.test(text);
|
|
283
|
+
}
|
|
311
284
|
async function selectableRunSkills(root) {
|
|
312
285
|
const state = await readRecommendationState(root);
|
|
313
286
|
const rows = await skillManagerRows(root, state);
|
|
@@ -366,6 +339,69 @@ async function recommendedItems(kind) {
|
|
|
366
339
|
}
|
|
367
340
|
return { items: [], plugins: [], externalTools: [], installPrep: [] };
|
|
368
341
|
}
|
|
342
|
+
function pluginSourceIdentity(source) {
|
|
343
|
+
return source.trim().replace(/^git:/, "").replace(/\.git$/i, "").replace(/\/$/, "").toLocaleLowerCase();
|
|
344
|
+
}
|
|
345
|
+
function nativePluginManager(ctx) {
|
|
346
|
+
const settingsManager = SettingsManager.create(ctx.cwd, getAgentDir(), { projectTrusted: ctx.isProjectTrusted() });
|
|
347
|
+
return new DefaultPackageManager({ cwd: ctx.cwd, agentDir: getAgentDir(), settingsManager });
|
|
348
|
+
}
|
|
349
|
+
function pluginDisplayName(source) {
|
|
350
|
+
const normalized = source.replace(/^git:/, "").replace(/[?#].*$/, "").replace(/\.git$/i, "").replace(/\/$/, "");
|
|
351
|
+
const tail = normalized.split(/[\\/]/).filter(Boolean).at(-1) ?? normalized;
|
|
352
|
+
return tail.replace(/^npm:/, "") || "Local Plugin";
|
|
353
|
+
}
|
|
354
|
+
async function pluginManagerRows(ctx) {
|
|
355
|
+
const catalog = await recommendedItems("skills");
|
|
356
|
+
const configuredBySource = new Map();
|
|
357
|
+
for (const entry of nativePluginManager(ctx).listConfiguredPackages()) {
|
|
358
|
+
const key = pluginSourceIdentity(entry.source);
|
|
359
|
+
const previous = configuredBySource.get(key);
|
|
360
|
+
if (!previous || entry.scope === "project")
|
|
361
|
+
configuredBySource.set(key, entry);
|
|
362
|
+
}
|
|
363
|
+
const matched = new Set();
|
|
364
|
+
const recommended = catalog.plugins.map((item) => {
|
|
365
|
+
const sourceRef = String(item.sourceRef ?? "").trim();
|
|
366
|
+
const sourceKey = pluginSourceIdentity(sourceRef);
|
|
367
|
+
const installed = configuredBySource.get(sourceKey);
|
|
368
|
+
if (installed)
|
|
369
|
+
matched.add(sourceKey);
|
|
370
|
+
return {
|
|
371
|
+
id: String(item.id ?? ""),
|
|
372
|
+
name: String(item.name ?? item.id ?? "Plugin"),
|
|
373
|
+
description: String(item.description ?? ""),
|
|
374
|
+
sourceRef,
|
|
375
|
+
installed: installed?.installedPath !== undefined,
|
|
376
|
+
installable: item.installable !== false,
|
|
377
|
+
local: false,
|
|
378
|
+
...(installed === undefined ? {} : { scope: installed.scope }),
|
|
379
|
+
...(installed?.filtered === undefined ? {} : { filtered: installed.filtered }),
|
|
380
|
+
...(typeof item.stage === "string" ? { stage: item.stage } : {}),
|
|
381
|
+
...(item.installable === false
|
|
382
|
+
? { reason: String(item.installHint ?? "尚未提供 Pi 可安装的 Plugin package") }
|
|
383
|
+
: installed && installed.installedPath === undefined
|
|
384
|
+
? { reason: "Pi 配置中已有该 Plugin,但本地安装目录不存在;请重新安装。" }
|
|
385
|
+
: {}),
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
const local = [...configuredBySource.entries()]
|
|
389
|
+
.filter(([key]) => !matched.has(key))
|
|
390
|
+
.map(([, entry], index) => ({
|
|
391
|
+
id: `local-plugin-${index + 1}`,
|
|
392
|
+
name: pluginDisplayName(entry.source),
|
|
393
|
+
description: "由 Pi 原生包管理器识别的本地已安装 Plugin。",
|
|
394
|
+
sourceRef: entry.source,
|
|
395
|
+
installed: entry.installedPath !== undefined,
|
|
396
|
+
installable: true,
|
|
397
|
+
local: true,
|
|
398
|
+
scope: entry.scope,
|
|
399
|
+
filtered: entry.filtered,
|
|
400
|
+
stage: "本地 Plugin",
|
|
401
|
+
...(entry.installedPath === undefined ? { reason: "Pi 配置中已有该 Plugin,但对应的本地安装目录不存在;请重新安装。" } : {}),
|
|
402
|
+
}));
|
|
403
|
+
return [...recommended, ...local];
|
|
404
|
+
}
|
|
369
405
|
function skillScopeLabel(scope) {
|
|
370
406
|
if (scope === "local")
|
|
371
407
|
return "用户本地目录(所有项目可发现)";
|
|
@@ -568,6 +604,79 @@ async function openMcpManager(ctx, rows) {
|
|
|
568
604
|
disabledText: "已关闭",
|
|
569
605
|
})));
|
|
570
606
|
}
|
|
607
|
+
function pluginManagerItems(rows) {
|
|
608
|
+
return rows.map((row) => ({
|
|
609
|
+
id: row.id,
|
|
610
|
+
name: row.name,
|
|
611
|
+
description: row.description,
|
|
612
|
+
status: !row.installable ? "blocked" : row.installed ? "enabled" : "missing",
|
|
613
|
+
sourceRef: row.sourceRef,
|
|
614
|
+
details: [
|
|
615
|
+
`类别:${row.stage ?? "研究扩展"}`,
|
|
616
|
+
...(row.scope ? [`安装位置:${row.scope === "project" ? "项目目录(仅当前项目)" : "系统目录(所有项目)"}`] : []),
|
|
617
|
+
...(row.filtered ? ["资源范围:使用了 Pi package 过滤配置"] : []),
|
|
618
|
+
],
|
|
619
|
+
...(row.reason ? { reason: row.reason } : {}),
|
|
620
|
+
}));
|
|
621
|
+
}
|
|
622
|
+
async function openPluginManager(ctx, rows) {
|
|
623
|
+
return ctx.ui.custom((tui, theme, keybindings, done) => (new SkillManagerComponent(pluginManagerItems(rows), tui, theme, keybindings, done, {
|
|
624
|
+
title: "Plugin 推荐",
|
|
625
|
+
itemLabel: "Plugin",
|
|
626
|
+
footer: "↑/↓ 移动 · Enter 安装或重新安装 · Esc 关闭",
|
|
627
|
+
enterAction: "install",
|
|
628
|
+
toggleEnabled: false,
|
|
629
|
+
enabledText: "已由 Pi 原生 Plugin 管理器安装;Enter 可重新安装",
|
|
630
|
+
missingMessage: "按 Enter 选择项目目录或系统目录并安装。",
|
|
631
|
+
})));
|
|
632
|
+
}
|
|
633
|
+
async function installRecommendedPlugin(ctx, row) {
|
|
634
|
+
if (!row.installable)
|
|
635
|
+
throw new Error(row.reason ?? `${row.name} 尚未提供可安装的 Plugin package`);
|
|
636
|
+
if (!row.sourceRef)
|
|
637
|
+
throw new Error(`推荐 Plugin 没有来源网址: ${row.id}`);
|
|
638
|
+
const location = await ctx.ui.select("选择 Plugin 安装位置", ["项目目录(仅当前项目)", "系统目录(所有项目)"]);
|
|
639
|
+
if (!location)
|
|
640
|
+
return;
|
|
641
|
+
const local = location.startsWith("项目目录");
|
|
642
|
+
const approved = await ctx.ui.confirm("使用 Pi 原生管理器安装 Plugin?", `${row.name}\n来源:${row.sourceRef}\n安装位置:${location}\n安装完成后需要执行 /reload。`);
|
|
643
|
+
if (!approved)
|
|
644
|
+
return;
|
|
645
|
+
await nativePluginManager(ctx).installAndPersist(row.sourceRef, { local });
|
|
646
|
+
ctx.ui.notify(`${row.name} 已安装到${location}。请执行 /reload 载入 Plugin。`, "info");
|
|
647
|
+
}
|
|
648
|
+
async function showPluginManager(args, ctx) {
|
|
649
|
+
const [verb, id] = args.trim().split(/\s+/).filter(Boolean);
|
|
650
|
+
if (verb && !["status", "install"].includes(verb))
|
|
651
|
+
throw new Error("直接运行 /plugin 打开 Plugin 管理页");
|
|
652
|
+
if (verb === "install" && !id)
|
|
653
|
+
throw new Error("直接运行 /plugin,在列表中选择要安装的 Plugin");
|
|
654
|
+
const rows = await pluginManagerRows(ctx);
|
|
655
|
+
if (verb === "install") {
|
|
656
|
+
const row = rows.find((candidate) => candidate.id === id);
|
|
657
|
+
if (!row)
|
|
658
|
+
throw new Error(`未找到推荐 Plugin: ${id}`);
|
|
659
|
+
await installRecommendedPlugin(ctx, row);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
|
|
663
|
+
ctx.ui.notify(rows.map((row) => `${row.installed ? "[on]" : "[off]"} ${row.id} — ${row.name}${row.scope ? `(${row.scope === "project" ? "项目" : "系统"})` : ""}`).join("\n"), "info");
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
while (true) {
|
|
667
|
+
const currentRows = await pluginManagerRows(ctx);
|
|
668
|
+
const action = await openPluginManager(ctx, currentRows);
|
|
669
|
+
if (action.type === "close")
|
|
670
|
+
return;
|
|
671
|
+
if (action.type !== "install")
|
|
672
|
+
continue;
|
|
673
|
+
const row = currentRows.find((candidate) => candidate.id === action.id);
|
|
674
|
+
if (!row)
|
|
675
|
+
continue;
|
|
676
|
+
await installRecommendedPlugin(ctx, row);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
571
680
|
async function setRecommendedMcpEnabled(root, id, enabled) {
|
|
572
681
|
const state = await readRecommendationState(root);
|
|
573
682
|
const current = new Set(state.mcp);
|
|
@@ -596,6 +705,16 @@ function modelMcpInstallTask(root, row, plan) {
|
|
|
596
705
|
`成功后确保 .psyclaw/recommendations.json 的 mcp 列表包含 ${JSON.stringify(row.id)},说明写入了哪些配置,并提醒用户执行 /reload。若安装命令本身失败,直接诊断并修复;只有遇到必须由用户提供的凭据或外部软件许可证时才向用户说明。`,
|
|
597
706
|
].join("\n");
|
|
598
707
|
}
|
|
708
|
+
function modelExternalToolInstallTask(root, item) {
|
|
709
|
+
return [
|
|
710
|
+
`安装推荐外部工具:${String(item.name ?? item.id ?? "external tool")}。`,
|
|
711
|
+
`官方来源:${String(item.sourceRef ?? "未提供;请停止并告知用户")}`,
|
|
712
|
+
`当前工作目录:${root}`,
|
|
713
|
+
String(item.installHint ?? "读取官方最新安装说明,选择适合当前操作系统的安装方式。"),
|
|
714
|
+
"用户已通过 /install 明确请求安装。直接读取官方说明、执行安装并验证版本或最小命令,不要再次请求安装许可。不要读取或输出凭据。",
|
|
715
|
+
"这是外部工具,不要将它宣称为来源仓库提供的 Skill 或 Pi Plugin。完成后报告实际安装位置、版本和验证结果;只有需要凭据、商业许可证或管理员提权时再请用户处理。",
|
|
716
|
+
].join("\n");
|
|
717
|
+
}
|
|
599
718
|
async function queueModelMcpInstall(pi, ctx, row, plan) {
|
|
600
719
|
if (!row.sourceRef)
|
|
601
720
|
throw new Error(`推荐 MCP 没有来源网址: ${row.id}`);
|
|
@@ -970,39 +1089,60 @@ export default function psyclawExtension(pi) {
|
|
|
970
1089
|
});
|
|
971
1090
|
if (!legacyTestApi && typeof pi.on === "function")
|
|
972
1091
|
pi.on("session_shutdown", () => runtimeMcps.close());
|
|
1092
|
+
if (!legacyTestApi && typeof pi.on === "function")
|
|
1093
|
+
pi.on("input", (event, ctx) => {
|
|
1094
|
+
if (event.source === "extension")
|
|
1095
|
+
return;
|
|
1096
|
+
if (!/^\/(?:login|logout)(?:\s|$)/i.test(event.text.trim()))
|
|
1097
|
+
return;
|
|
1098
|
+
ctx.ui.notify("PsyClaw 已统一隐藏底层登录命令。请使用 /provider 配置、切换或更新模型凭据。", "info");
|
|
1099
|
+
return { action: "handled" };
|
|
1100
|
+
});
|
|
973
1101
|
if (!legacyTestApi && typeof pi.on === "function")
|
|
974
1102
|
pi.on("tool_call", async (event, ctx) => {
|
|
975
1103
|
const run = await readControlledRun(ctx.cwd);
|
|
976
1104
|
if (!run || !toolNeedsApproval(event.toolName, event.input))
|
|
977
1105
|
return;
|
|
978
1106
|
const summary = toolApprovalSummary(event.toolName, event.input);
|
|
979
|
-
|
|
980
|
-
|
|
1107
|
+
const blockedReason = autoApprovalBlocked(event.toolName, event.input);
|
|
1108
|
+
if (blockedReason) {
|
|
981
1109
|
await appendApproval(ctx.cwd, {
|
|
982
1110
|
kind: "tool",
|
|
983
1111
|
nodeId: event.toolCallId,
|
|
984
|
-
decision:
|
|
1112
|
+
decision: "rejected",
|
|
985
1113
|
actor: "auto",
|
|
986
1114
|
runId: run.runId,
|
|
987
1115
|
sha256: approvalInputDigest({ toolName: event.toolName, input: event.input }),
|
|
988
|
-
summary: blockedReason
|
|
1116
|
+
summary: blockedReason,
|
|
989
1117
|
});
|
|
990
|
-
return
|
|
1118
|
+
return { block: true, terminate: true, reason: blockedReason };
|
|
1119
|
+
}
|
|
1120
|
+
if (requiresSeparateOperationConfirmation(event.toolName, event.input)) {
|
|
1121
|
+
if (!ctx.hasUI)
|
|
1122
|
+
return { block: true, terminate: true, reason: "外部发布需要用户在交互界面中明确确认" };
|
|
1123
|
+
const choice = await ctx.ui.select(`确认外部操作\n${summary}`, ["确认执行", "取消"], { timeout: 120_000 });
|
|
1124
|
+
const approved = choice === "确认执行";
|
|
1125
|
+
await appendApproval(ctx.cwd, {
|
|
1126
|
+
kind: "tool",
|
|
1127
|
+
nodeId: event.toolCallId,
|
|
1128
|
+
decision: approved ? "approved" : "rejected",
|
|
1129
|
+
actor: "human",
|
|
1130
|
+
runId: run.runId,
|
|
1131
|
+
sha256: approvalInputDigest({ toolName: event.toolName, input: event.input }),
|
|
1132
|
+
summary,
|
|
1133
|
+
});
|
|
1134
|
+
return approved ? undefined : { block: true, terminate: true, reason: "用户取消了外部操作" };
|
|
991
1135
|
}
|
|
992
|
-
if (!ctx.hasUI)
|
|
993
|
-
return { block: true, terminate: true, reason: "该步骤需要人在环路审批,但当前没有交互界面" };
|
|
994
|
-
const choice = await ctx.ui.select(`审批执行步骤\n${summary}`, ["批准本次执行", "拒绝并停止"], { timeout: 120_000 });
|
|
995
|
-
const approved = choice === "批准本次执行";
|
|
996
1136
|
await appendApproval(ctx.cwd, {
|
|
997
1137
|
kind: "tool",
|
|
998
1138
|
nodeId: event.toolCallId,
|
|
999
|
-
decision:
|
|
1000
|
-
actor: "
|
|
1139
|
+
decision: "auto-approved",
|
|
1140
|
+
actor: "auto",
|
|
1001
1141
|
runId: run.runId,
|
|
1002
1142
|
sha256: approvalInputDigest({ toolName: event.toolName, input: event.input }),
|
|
1003
1143
|
summary,
|
|
1004
1144
|
});
|
|
1005
|
-
return
|
|
1145
|
+
return undefined;
|
|
1006
1146
|
});
|
|
1007
1147
|
pi.registerCommand("init", {
|
|
1008
1148
|
description: "初始化可追溯的研究项目",
|
|
@@ -1023,18 +1163,6 @@ export default function psyclawExtension(pi) {
|
|
|
1023
1163
|
}
|
|
1024
1164
|
},
|
|
1025
1165
|
});
|
|
1026
|
-
if (!legacyTestApi)
|
|
1027
|
-
pi.registerCommand("approve", {
|
|
1028
|
-
description: "审批 /init 生成的主要研究计划文档",
|
|
1029
|
-
handler: async (_args, ctx) => {
|
|
1030
|
-
try {
|
|
1031
|
-
await reviewPrimaryDocuments(ctx);
|
|
1032
|
-
}
|
|
1033
|
-
catch (error) {
|
|
1034
|
-
await notifyError(ctx, error);
|
|
1035
|
-
}
|
|
1036
|
-
},
|
|
1037
|
-
});
|
|
1038
1166
|
if (!legacyTestApi)
|
|
1039
1167
|
pi.registerCommand("run", {
|
|
1040
1168
|
description: "启动受控研究流程,可用 --skills a,b 选择本次优化 Skill",
|
|
@@ -1045,61 +1173,28 @@ export default function psyclawExtension(pi) {
|
|
|
1045
1173
|
try {
|
|
1046
1174
|
project = await readProject(ctx.cwd);
|
|
1047
1175
|
}
|
|
1048
|
-
catch
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
// outside the /init flow can be analyzed directly.
|
|
1052
|
-
const { bootstrapProjectFromAnalysisDocs } = await import("../../project/bootstrap.js");
|
|
1053
|
-
try {
|
|
1054
|
-
project = await bootstrapProjectFromAnalysisDocs(ctx.cwd);
|
|
1055
|
-
}
|
|
1056
|
-
catch (bootstrapError) {
|
|
1057
|
-
ctx.ui.notify(bootstrapError instanceof Error ? bootstrapError.message : String(bootstrapError), "warning");
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
ctx.ui.notify("检测到合规的分析文档,已自动补齐研究项目记录并启动受控流程。", "info");
|
|
1061
|
-
}
|
|
1062
|
-
const planApproval = await primaryPlanApprovalStatus(ctx.cwd);
|
|
1063
|
-
if (!planApproval.ok) {
|
|
1064
|
-
const missing = planApproval.documents.filter((item) => !item.approved).map((item) => item.path);
|
|
1065
|
-
// `/run` is the sole switch into the controlled workflow. A plan
|
|
1066
|
-
// can still be incomplete when execution starts; keep the status
|
|
1067
|
-
// visible and let the model repair it through the normal workflow
|
|
1068
|
-
// instead of forcing a separate approval command first.
|
|
1069
|
-
ctx.ui.notify(`检测到计划尚未确认或已修改:${missing.join("、")}。将由受控流程继续处理;如需先人工确认,可运行 /approve。`, "warning");
|
|
1176
|
+
catch {
|
|
1177
|
+
ctx.ui.notify("当前目录还没有通过 /init 建立的研究记录,因此保持普通对话模式。需要受控研究流程时,请先运行 /init,再运行 /run。", "warning");
|
|
1178
|
+
return;
|
|
1070
1179
|
}
|
|
1071
1180
|
const available = await selectableRunSkills(ctx.cwd);
|
|
1072
|
-
|
|
1073
|
-
if (parsed.mode === "auto" && requestedSkills === undefined)
|
|
1074
|
-
requestedSkills = [];
|
|
1075
|
-
if (requestedSkills === undefined && ctx.hasUI && typeof ctx.ui.input === "function") {
|
|
1076
|
-
const answer = await ctx.ui.input("选择本次 Run 使用的 Skill(逗号分隔,可留空)", available.map((skill) => skill.name).join(", "));
|
|
1077
|
-
if (answer === undefined) {
|
|
1078
|
-
ctx.ui.notify("已取消启动受控研究流程", "info");
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
|
-
requestedSkills = answer.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1082
|
-
}
|
|
1083
|
-
const selectedSkills = resolveRunSkills(requestedSkills ?? [], available);
|
|
1181
|
+
const selectedSkills = resolveRunSkills(parsed.requestedSkills ?? [], available);
|
|
1084
1182
|
const objective = parsed.objective || project.goal;
|
|
1085
1183
|
const runId = `run_${Date.now()}`;
|
|
1086
1184
|
await appendApproval(ctx.cwd, {
|
|
1087
1185
|
kind: "run-mode",
|
|
1088
1186
|
nodeId: "run-mode",
|
|
1089
|
-
decision:
|
|
1090
|
-
actor:
|
|
1187
|
+
decision: "approved",
|
|
1188
|
+
actor: "human",
|
|
1091
1189
|
runId,
|
|
1092
1190
|
sha256: approvalInputDigest({ projectId: project.id, objective, selectedSkills, mode: parsed.mode }),
|
|
1093
|
-
summary:
|
|
1191
|
+
summary: "用户通过 /run 启动受控研究流程;计划内常规操作连续执行,仅研究取舍分歧请求决策",
|
|
1094
1192
|
});
|
|
1095
|
-
if (!(await approveRunNodes(ctx, runId, parsed.mode))) {
|
|
1096
|
-
ctx.ui.notify("运行审批被拒绝或超时,未启动。", "warning");
|
|
1097
|
-
return;
|
|
1098
|
-
}
|
|
1099
1193
|
await activateControlledRun(ctx.cwd, runId, project.id, objective, selectedSkills, parsed.mode);
|
|
1194
|
+
await new RunEventLog(ctx.cwd, runId).append({ type: "planned", at: new Date().toISOString(), message: objective });
|
|
1100
1195
|
pi.appendEntry("psyclaw:controlled-run", { runId, projectId: project.id, objective, selectedSkills, mode: parsed.mode, activatedAt: new Date().toISOString() });
|
|
1101
1196
|
pi.sendUserMessage(controlledRunRequest(objective, selectedSkills, parsed.mode), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
1102
|
-
ctx.ui.notify(
|
|
1197
|
+
ctx.ui.notify(`受控研究流程已启动${selectedSkills.length > 0 ? `;本次使用 Skill:${selectedSkills.join(", ")}` : ";使用默认 Skill"}。仅在研究分析存在需要你取舍的实质分歧时暂停询问。`, "info");
|
|
1103
1198
|
}
|
|
1104
1199
|
catch (error) {
|
|
1105
1200
|
await notifyError(ctx, error);
|
|
@@ -1135,6 +1230,10 @@ export default function psyclawExtension(pi) {
|
|
|
1135
1230
|
description: "运行离线证据门控研究简报",
|
|
1136
1231
|
handler: async (_args, ctx) => {
|
|
1137
1232
|
try {
|
|
1233
|
+
if (!(await readControlledRun(ctx.cwd))) {
|
|
1234
|
+
ctx.ui.notify("当前是普通对话模式。需要生成可追溯研究简报时,请先运行 /init,再运行 /run。", "warning");
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1138
1237
|
const result = await runOfflineBrief(ctx.cwd);
|
|
1139
1238
|
ctx.ui.notify(result.verdict === "pass" ? `Brief ready: ${result.briefPath}` : "Brief blocked by evidence gates", result.verdict === "pass" ? "info" : "warning");
|
|
1140
1239
|
}
|
|
@@ -1166,13 +1265,7 @@ export default function psyclawExtension(pi) {
|
|
|
1166
1265
|
ctx.ui.notify("请先使用 /run 启动受控研究流程;普通对话模式不会自动执行同行评审。", "warning");
|
|
1167
1266
|
return;
|
|
1168
1267
|
}
|
|
1169
|
-
const reviewSkill =
|
|
1170
|
-
? await ctx.ui.input("同行评审使用哪个 Skill?", "留空使用 PsyClaw 默认多角色模拟评审")
|
|
1171
|
-
: undefined;
|
|
1172
|
-
if (reviewSkill === undefined && ctx.hasUI) {
|
|
1173
|
-
ctx.ui.notify("已取消同行评审", "info");
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1268
|
+
const reviewSkill = _args.trim();
|
|
1176
1269
|
activeAgentRuns.add(ctx.cwd);
|
|
1177
1270
|
ctx.ui.notify(`正在使用${reviewSkill?.trim() ? ` Skill“${reviewSkill.trim()}”辅助的` : "默认"} 4 角色并发模拟评审;不会自动改稿。`, "info");
|
|
1178
1271
|
const result = await runParallelPeerReview({
|
|
@@ -1181,14 +1274,14 @@ export default function psyclawExtension(pi) {
|
|
|
1181
1274
|
...(ctx.model?.provider === undefined ? {} : { provider: ctx.model.provider }),
|
|
1182
1275
|
...(ctx.model?.id === undefined ? {} : { model: ctx.model.id }),
|
|
1183
1276
|
env: providerEnvironment(ctx.model?.provider),
|
|
1184
|
-
...(reviewSkill
|
|
1277
|
+
...(reviewSkill ? { skillGuidance: reviewSkill } : {}),
|
|
1185
1278
|
onEvent: async (event) => { await new RunEventLog(ctx.cwd, event.runId).append(event); },
|
|
1186
1279
|
});
|
|
1187
1280
|
pi.appendEntry("psyclaw:multi-agent-review", {
|
|
1188
1281
|
runId: result.runId,
|
|
1189
1282
|
status: result.status,
|
|
1190
1283
|
outputPath: result.outputPath,
|
|
1191
|
-
adoption: "
|
|
1284
|
+
adoption: "review-output-only",
|
|
1192
1285
|
recordedAt: new Date().toISOString(),
|
|
1193
1286
|
});
|
|
1194
1287
|
ctx.ui.notify(`模拟审稿${result.status === "completed" ? "已完成" : "未完整完成"}:${result.outputPath}。审稿意见仅供研究者决定是否采纳。`, result.status === "completed" ? "info" : "warning");
|
|
@@ -1223,13 +1316,17 @@ export default function psyclawExtension(pi) {
|
|
|
1223
1316
|
ctx.ui.notify("请先使用 /init 初始化研究项目,再运行 /loop", "warning");
|
|
1224
1317
|
return;
|
|
1225
1318
|
}
|
|
1319
|
+
if (!(await readControlledRun(ctx.cwd))) {
|
|
1320
|
+
ctx.ui.notify("项目已初始化,但尚未启动受控研究流程。请运行 /run 后再使用 /loop;当前继续保持普通对话模式。", "warning");
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1226
1323
|
const objective = requested || project.goal;
|
|
1227
1324
|
const request = [
|
|
1228
1325
|
"运行一个有边界的 PsyClaw 研究循环,采用计划、执行、检查、修正的顺序推进当前研究阶段。",
|
|
1229
1326
|
`本轮目标:${objective}`,
|
|
1230
1327
|
"开始前读取 .psyclaw/project.json、notes/goal.md 和 notes/plan.md;如果当前对话或项目中存在经研究者确认的 /grill 研究规格,以该规格细化初始计划,但不得违反系统规则、AGENTS.md 或研究门禁。",
|
|
1231
|
-
"
|
|
1232
|
-
"
|
|
1328
|
+
"先识别当前最高优先级且前提已满足的一个任务,再使用可用工具实际推进。不要只描述准备做什么。普通项目文件写入、脚本和可恢复工具调用按 /run 的范围授权连续推进;原始数据覆盖、凭据、外部发布、破坏性操作仍遵守单独的安全边界。",
|
|
1329
|
+
"每完成一个实质步骤就检查产物、证据和计划是否一致。缺失信息若能检索、计算、重跑、改写或明确标注,应先自行修复。只有存在会改变研究问题、样本处理、操作化、估计目标、方法或解释的实质分歧时才请求研究者取舍;权限边界、不可恢复错误或当前阶段完成时停止。不得为了继续循环而虚构输入或结论。",
|
|
1233
1330
|
"本次最多推进一个可验收的研究阶段。结束时简要报告已完成、已核验、阻塞、计划变化和下一步;规划内容不得表述为已有研究证据。",
|
|
1234
1331
|
].join("\n");
|
|
1235
1332
|
pi.sendUserMessage(request, ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
@@ -1279,19 +1376,32 @@ export default function psyclawExtension(pi) {
|
|
|
1279
1376
|
});
|
|
1280
1377
|
if (!legacyTestApi)
|
|
1281
1378
|
pi.registerCommand("plugin", {
|
|
1282
|
-
description: "
|
|
1379
|
+
description: "浏览推荐并管理 Plugin / Extension",
|
|
1283
1380
|
handler: async (args, ctx) => {
|
|
1284
1381
|
try {
|
|
1285
1382
|
const [action, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
1286
1383
|
if (!action) {
|
|
1287
|
-
|
|
1384
|
+
await showPluginManager("", ctx);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
if (action === "status") {
|
|
1388
|
+
await showPluginManager("status", ctx);
|
|
1288
1389
|
return;
|
|
1289
1390
|
}
|
|
1290
1391
|
if (!["list", "install", "remove"].includes(action)) {
|
|
1291
|
-
throw new Error("
|
|
1392
|
+
throw new Error("直接运行 /plugin 打开 Plugin 管理页");
|
|
1393
|
+
}
|
|
1394
|
+
if (action !== "list" && rest.length === 0) {
|
|
1395
|
+
await showPluginManager("", ctx);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
if (action === "install" && rest.length === 1) {
|
|
1399
|
+
const recommended = (await pluginManagerRows(ctx)).some((row) => row.id === rest[0]);
|
|
1400
|
+
if (recommended) {
|
|
1401
|
+
await showPluginManager(`install ${rest[0]}`, ctx);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1292
1404
|
}
|
|
1293
|
-
if (action !== "list" && rest.length === 0)
|
|
1294
|
-
throw new Error(`Usage: /plugin ${action} <source> [-l]`);
|
|
1295
1405
|
if (action !== "list") {
|
|
1296
1406
|
const approved = await ctx.ui.confirm(`${action === "install" ? "安装" : "移除"} Plugin?`, `${rest.join(" ")}\nPlugin 与宿主进程同权限,操作完成后需要重启 PsyClaw。`);
|
|
1297
1407
|
if (!approved)
|
|
@@ -1299,7 +1409,7 @@ export default function psyclawExtension(pi) {
|
|
|
1299
1409
|
}
|
|
1300
1410
|
await runPluginCommand([action, ...rest]);
|
|
1301
1411
|
if (action !== "list")
|
|
1302
|
-
ctx.ui.notify("Plugin
|
|
1412
|
+
ctx.ui.notify("Plugin 配置已更新,请执行 /reload。", "info");
|
|
1303
1413
|
}
|
|
1304
1414
|
catch (error) {
|
|
1305
1415
|
await notifyError(ctx, error);
|
|
@@ -1328,24 +1438,34 @@ export default function psyclawExtension(pi) {
|
|
|
1328
1438
|
const [skills, mcps] = await Promise.all([recommendedItems("skills"), recommendedItems("mcp")]);
|
|
1329
1439
|
const skillLines = skills.items.slice(0, 8).map((item) => `Skill: ${String(item.id)} — ${String(item.name)}`);
|
|
1330
1440
|
const mcpLines = mcps.items.slice(0, 8).map((item) => `MCP: ${String(item.id)} — ${String(item.name)}`);
|
|
1331
|
-
const pluginLines = skills.plugins.slice(0, 8).map((item) => `Plugin: ${String(item.
|
|
1332
|
-
const toolLines = skills.externalTools.slice(0, 8).map((item) => `外部工具: ${String(item.
|
|
1333
|
-
ctx.ui.notify(["推荐安装入口", "", ...skillLines, ...pluginLines, ...mcpLines, "", ...toolLines, "", "安装:/install skill|mcp <id>", "Plugin:/plugin install <
|
|
1441
|
+
const pluginLines = skills.plugins.slice(0, 8).map((item) => `Plugin: ${String(item.id)} — ${String(item.name)}`);
|
|
1442
|
+
const toolLines = skills.externalTools.slice(0, 8).map((item) => `外部工具: ${String(item.id)} — ${String(item.name)}`);
|
|
1443
|
+
ctx.ui.notify(["推荐安装入口", "", ...skillLines, ...pluginLines, ...mcpLines, "", ...toolLines, "", "安装:/install skill|mcp|external <id>", "Plugin:/plugin 或 /plugin install <id>", "管理:/skill、/plugin 或 /mcp", "也可以打开 /panel 查看推荐页面"].join("\n"), "info");
|
|
1334
1444
|
return;
|
|
1335
1445
|
}
|
|
1336
|
-
if (kind !== "skill" && kind !== "mcp")
|
|
1337
|
-
throw new Error("Usage: /install skill|mcp <id>");
|
|
1446
|
+
if (kind !== "skill" && kind !== "mcp" && kind !== "external")
|
|
1447
|
+
throw new Error("Usage: /install skill|mcp|external <id>");
|
|
1338
1448
|
if (!id) {
|
|
1339
1449
|
if (kind === "skill")
|
|
1340
1450
|
await showSkillManager(pi, "", ctx);
|
|
1341
|
-
else
|
|
1451
|
+
else if (kind === "mcp")
|
|
1342
1452
|
await showMcpManager(pi, "", ctx, runtimeMcps);
|
|
1453
|
+
else
|
|
1454
|
+
ctx.ui.notify("Usage: /install external <id>", "info");
|
|
1343
1455
|
return;
|
|
1344
1456
|
}
|
|
1345
1457
|
if (kind === "skill")
|
|
1346
1458
|
await showSkillManager(pi, `install ${id}`, ctx);
|
|
1347
|
-
else
|
|
1459
|
+
else if (kind === "mcp")
|
|
1348
1460
|
await showMcpManager(pi, `install ${id}`, ctx, runtimeMcps);
|
|
1461
|
+
else {
|
|
1462
|
+
const skills = await recommendedItems("skills");
|
|
1463
|
+
const item = skills.externalTools.find((candidate) => candidate.id === id && candidate.installable !== false);
|
|
1464
|
+
if (!item)
|
|
1465
|
+
throw new Error(`未找到可安装的推荐外部工具: ${id}`);
|
|
1466
|
+
pi.sendUserMessage(modelExternalToolInstallTask(ctx.cwd, item), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
1467
|
+
ctx.ui.notify(`已将 ${String(item.name ?? id)} 的安装任务交给当前模型;完成后会报告版本与验证结果。`, "info");
|
|
1468
|
+
}
|
|
1349
1469
|
}
|
|
1350
1470
|
catch (error) {
|
|
1351
1471
|
await notifyError(ctx, error);
|
|
@@ -1468,6 +1588,138 @@ export default function psyclawExtension(pi) {
|
|
|
1468
1588
|
},
|
|
1469
1589
|
});
|
|
1470
1590
|
if (typeof pi.registerTool === "function") {
|
|
1591
|
+
pi.registerTool({
|
|
1592
|
+
name: "psyclaw_research_decision",
|
|
1593
|
+
label: "Research decision",
|
|
1594
|
+
description: "Request researcher input only for an unresolved substantive research trade-off. Never use this for software, formatting, missing reporting fields, recoverable errors, evidence collection, credentials, publication, or other operational authorization.",
|
|
1595
|
+
parameters: Type.Object({
|
|
1596
|
+
id: Type.String({ minLength: 1 }),
|
|
1597
|
+
question: Type.String({ minLength: 1 }),
|
|
1598
|
+
options: Type.Array(Type.Object({
|
|
1599
|
+
id: Type.String({ minLength: 1 }),
|
|
1600
|
+
label: Type.String({ minLength: 1 }),
|
|
1601
|
+
rationale: Type.String({ minLength: 1 }),
|
|
1602
|
+
consequences: Type.Array(Type.String()),
|
|
1603
|
+
evidenceRefs: Type.Array(Type.String()),
|
|
1604
|
+
}), { minItems: 2 }),
|
|
1605
|
+
affectedAreas: Type.Array(Type.Union([
|
|
1606
|
+
Type.Literal("research-question"),
|
|
1607
|
+
Type.Literal("sample-treatment"),
|
|
1608
|
+
Type.Literal("operationalization"),
|
|
1609
|
+
Type.Literal("estimand"),
|
|
1610
|
+
Type.Literal("analysis-method"),
|
|
1611
|
+
Type.Literal("interpretation"),
|
|
1612
|
+
]), { minItems: 1 }),
|
|
1613
|
+
evidenceCannotResolve: Type.Boolean(),
|
|
1614
|
+
evidenceReviewed: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
1615
|
+
methodsChecked: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
1616
|
+
unresolvedReason: Type.String({ minLength: 1 }),
|
|
1617
|
+
recommendation: Type.String({ minLength: 1 }),
|
|
1618
|
+
}),
|
|
1619
|
+
executionMode: "sequential",
|
|
1620
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1621
|
+
const run = await readControlledRun(ctx.cwd);
|
|
1622
|
+
if (!run) {
|
|
1623
|
+
return {
|
|
1624
|
+
content: [{ type: "text", text: "当前是普通对话模式,不能创建研究决策状态。请仅在用户先执行 /init、再执行 /run 后使用本工具。" }],
|
|
1625
|
+
details: { status: "general-mode" },
|
|
1626
|
+
isError: true,
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
const request = {
|
|
1630
|
+
schemaVersion: "psyclaw/research-decision/v1",
|
|
1631
|
+
id: params.id,
|
|
1632
|
+
question: params.question,
|
|
1633
|
+
options: params.options,
|
|
1634
|
+
affectedAreas: params.affectedAreas,
|
|
1635
|
+
evidenceCannotResolve: params.evidenceCannotResolve,
|
|
1636
|
+
evidenceReviewed: params.evidenceReviewed,
|
|
1637
|
+
methodsChecked: params.methodsChecked,
|
|
1638
|
+
unresolvedReason: params.unresolvedReason,
|
|
1639
|
+
recommendation: params.recommendation,
|
|
1640
|
+
};
|
|
1641
|
+
try {
|
|
1642
|
+
assertResearchDecision(request);
|
|
1643
|
+
}
|
|
1644
|
+
catch (error) {
|
|
1645
|
+
return {
|
|
1646
|
+
content: [{ type: "text", text: `该事项不符合研究取舍条件:${error instanceof Error ? error.message : String(error)}。请继续检索、计算、修复,或按单独的安全授权边界处理。` }],
|
|
1647
|
+
details: { status: "not-a-research-decision", runId: run.runId },
|
|
1648
|
+
isError: true,
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
const eventLog = new RunEventLog(ctx.cwd, run.runId);
|
|
1652
|
+
const events = await eventLog.snapshot();
|
|
1653
|
+
const resolvedIds = new Set(events
|
|
1654
|
+
.filter((event) => event.type === "decision-resolved" && event.researchDecisionResolution)
|
|
1655
|
+
.map((event) => event.researchDecisionResolution.decisionId));
|
|
1656
|
+
const pending = findLastMatching(events, (event) => event.type === "awaiting-human" && Boolean(event.researchDecision && !resolvedIds.has(event.researchDecision.id)));
|
|
1657
|
+
if (pending?.researchDecision) {
|
|
1658
|
+
return {
|
|
1659
|
+
content: [{ type: "text", text: `当前已有一项待研究者取舍的问题:${pending.researchDecision.question}。请先处理该问题,不能同时创建第二项。` }],
|
|
1660
|
+
details: { status: "research-decision-already-pending", runId: run.runId, decisionId: pending.researchDecision.id },
|
|
1661
|
+
isError: true,
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
if (events.some((event) => event.researchDecision?.id === request.id || event.researchDecisionResolution?.decisionId === request.id)) {
|
|
1665
|
+
return {
|
|
1666
|
+
content: [{ type: "text", text: `研究取舍编号已使用:${request.id}` }],
|
|
1667
|
+
details: { status: "duplicate-research-decision", runId: run.runId, decisionId: request.id },
|
|
1668
|
+
isError: true,
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
await eventLog.append({
|
|
1672
|
+
type: "awaiting-human",
|
|
1673
|
+
at: new Date().toISOString(),
|
|
1674
|
+
researchDecision: request,
|
|
1675
|
+
message: request.question,
|
|
1676
|
+
});
|
|
1677
|
+
return {
|
|
1678
|
+
content: [{ type: "text", text: JSON.stringify({ status: "需要研究者取舍", runId: run.runId, decision: request }, null, 2) }],
|
|
1679
|
+
details: { status: "awaiting-human", runId: run.runId, decisionId: request.id },
|
|
1680
|
+
};
|
|
1681
|
+
},
|
|
1682
|
+
});
|
|
1683
|
+
pi.registerTool({
|
|
1684
|
+
name: "psyclaw_resolve_research_decision",
|
|
1685
|
+
label: "Resolve research decision",
|
|
1686
|
+
description: "Record the researcher's answer to the current substantive research trade-off and resume the controlled workflow. This is not an operational approval tool.",
|
|
1687
|
+
parameters: Type.Object({
|
|
1688
|
+
decisionId: Type.String({ minLength: 1 }),
|
|
1689
|
+
selectedOption: Type.String({ minLength: 1 }),
|
|
1690
|
+
rationale: Type.String({ minLength: 1 }),
|
|
1691
|
+
}),
|
|
1692
|
+
executionMode: "sequential",
|
|
1693
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1694
|
+
const run = await readControlledRun(ctx.cwd);
|
|
1695
|
+
if (!run) {
|
|
1696
|
+
return { content: [{ type: "text", text: "当前没有有效的受控研究流程。" }], details: { status: "general-mode" }, isError: true };
|
|
1697
|
+
}
|
|
1698
|
+
const eventLog = new RunEventLog(ctx.cwd, run.runId);
|
|
1699
|
+
const events = await eventLog.snapshot();
|
|
1700
|
+
const resolvedIds = new Set(events
|
|
1701
|
+
.filter((event) => event.type === "decision-resolved" && event.researchDecisionResolution)
|
|
1702
|
+
.map((event) => event.researchDecisionResolution.decisionId));
|
|
1703
|
+
const pending = findLastMatching(events, (event) => event.type === "awaiting-human" && event.researchDecision?.id === params.decisionId && !resolvedIds.has(params.decisionId));
|
|
1704
|
+
if (!pending?.researchDecision) {
|
|
1705
|
+
return { content: [{ type: "text", text: `未找到待处理的研究取舍:${params.decisionId}` }], details: { status: "not-found", runId: run.runId }, isError: true };
|
|
1706
|
+
}
|
|
1707
|
+
try {
|
|
1708
|
+
const resolution = resolveResearchDecision(pending.researchDecision, {
|
|
1709
|
+
selectedOption: params.selectedOption,
|
|
1710
|
+
rationale: params.rationale,
|
|
1711
|
+
});
|
|
1712
|
+
await eventLog.append({ type: "decision-resolved", at: resolution.resolvedAt, researchDecisionResolution: resolution });
|
|
1713
|
+
return {
|
|
1714
|
+
content: [{ type: "text", text: JSON.stringify({ status: "executing", runId: run.runId, resolution }, null, 2) }],
|
|
1715
|
+
details: { status: "executing", runId: run.runId, decisionId: resolution.decisionId },
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
catch (error) {
|
|
1719
|
+
return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], details: { status: "invalid-resolution", runId: run.runId }, isError: true };
|
|
1720
|
+
}
|
|
1721
|
+
},
|
|
1722
|
+
});
|
|
1471
1723
|
pi.registerTool({
|
|
1472
1724
|
name: "psyclaw_mcp",
|
|
1473
1725
|
label: "MCP tools",
|
|
@@ -1486,6 +1738,13 @@ export default function psyclawExtension(pi) {
|
|
|
1486
1738
|
const servers = await runtimeMcps.list(ctx.cwd, params.server);
|
|
1487
1739
|
return { content: [{ type: "text", text: JSON.stringify({ servers }, null, 2) }], details: { action: "list", servers: servers.length } };
|
|
1488
1740
|
}
|
|
1741
|
+
if (!(await readControlledRun(ctx.cwd))) {
|
|
1742
|
+
return {
|
|
1743
|
+
content: [{ type: "text", text: "当前是普通对话模式。可以查看 MCP 工具,但实际调用需先运行 /init,再运行 /run。" }],
|
|
1744
|
+
details: { action: "call", status: "general-mode" },
|
|
1745
|
+
isError: true,
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1489
1748
|
if (!params.server || !params.tool)
|
|
1490
1749
|
throw new Error("server and tool are required for action=call");
|
|
1491
1750
|
const result = await runtimeMcps.call(ctx.cwd, params.server, params.tool, params.input ?? {});
|
|
@@ -1535,7 +1794,7 @@ export default function psyclawExtension(pi) {
|
|
|
1535
1794
|
pi.registerTool({
|
|
1536
1795
|
name: "psyclaw_workbench",
|
|
1537
1796
|
label: "Research workbench",
|
|
1538
|
-
description: "Primary psyclaw workbench for durable research state, evidence tracking, academic analysis/report contracts,
|
|
1797
|
+
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.",
|
|
1539
1798
|
parameters: Type.Object({
|
|
1540
1799
|
request: Type.String({ description: "The user's research task in natural language" }),
|
|
1541
1800
|
identifier: Type.Optional(Type.String({ description: "DOI, publisher URL, or exact paper title when relevant" })),
|
|
@@ -1621,7 +1880,7 @@ export default function psyclawExtension(pi) {
|
|
|
1621
1880
|
confirmBeforeStart: true,
|
|
1622
1881
|
message: "文献检索与多源核验、全稿写作、DOCX 导出会消耗较多 token 与时间(长会话下缓存重读是主要费用)。开始前将告知预估 token/费用/时间,并可在更小范围或分段会话中执行。",
|
|
1623
1882
|
},
|
|
1624
|
-
message: "Proceed through the four core skills
|
|
1883
|
+
message: "Proceed through the four core skills. Before presenting the report as final, resolve or plainly disclose outstanding citations, figures, and document-format requirements; keep internal workflow status labels out of reader-facing prose.",
|
|
1625
1884
|
}, null, 2) }],
|
|
1626
1885
|
details: { status: "plan-required", workflow: "academic-analysis-report" },
|
|
1627
1886
|
};
|
|
@@ -1641,11 +1900,11 @@ export default function psyclawExtension(pi) {
|
|
|
1641
1900
|
content: [{ type: "text", text: JSON.stringify({
|
|
1642
1901
|
schemaVersion: "psyclaw/workbench-result/v1",
|
|
1643
1902
|
workflow: "institutional-fulltext",
|
|
1644
|
-
status: "awaiting-
|
|
1903
|
+
status: "awaiting-access-confirmation",
|
|
1645
1904
|
verdict: result.verdict,
|
|
1646
1905
|
runId: result.runId,
|
|
1647
1906
|
outputs: result.outputPaths,
|
|
1648
|
-
next: "Ask the user to complete institutional login in the visible browser and confirm the exact article before any download.",
|
|
1907
|
+
next: "Ask the user to complete institutional login in the visible browser and confirm the exact article before any download. This is an access-control confirmation, not a research-method decision.",
|
|
1649
1908
|
}, null, 2) }],
|
|
1650
1909
|
details: { runId: result.runId, verdict: result.verdict, outputPaths: result.outputPaths, approval: "pending" },
|
|
1651
1910
|
};
|
|
@@ -1751,13 +2010,8 @@ export default function psyclawExtension(pi) {
|
|
|
1751
2010
|
ctx.ui.notify("Agent task is too long; split it into smaller bounded tasks", "error");
|
|
1752
2011
|
return;
|
|
1753
2012
|
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
return;
|
|
1757
|
-
}
|
|
1758
|
-
// Do not let the runner create a partial `.psyclaw/runs` tree for an
|
|
1759
|
-
// uninitialized or corrupt project. `/init` is
|
|
1760
|
-
// the explicit state-creation step.
|
|
2013
|
+
// Do not let the runner create a partial `.psyclaw/runs` tree outside
|
|
2014
|
+
// an explicitly activated `/init` -> `/run` research workflow.
|
|
1761
2015
|
try {
|
|
1762
2016
|
await readProject(ctx.cwd);
|
|
1763
2017
|
}
|
|
@@ -1765,11 +2019,6 @@ export default function psyclawExtension(pi) {
|
|
|
1765
2019
|
ctx.ui.notify("请先使用 /init 初始化研究项目,再运行 /agents", "warning");
|
|
1766
2020
|
return;
|
|
1767
2021
|
}
|
|
1768
|
-
const approved = await ctx.ui.confirm("Run read-only research worker?", "The worker runs in a separate PsyClaw process with extensions, skills, context files, and mutating tools disabled.");
|
|
1769
|
-
if (!approved) {
|
|
1770
|
-
ctx.ui.notify("Agent run canceled", "info");
|
|
1771
|
-
return;
|
|
1772
|
-
}
|
|
1773
2022
|
if (activeAgentRuns.has(ctx.cwd)) {
|
|
1774
2023
|
ctx.ui.notify("A psyclaw agent run is already active for this project", "warning");
|
|
1775
2024
|
return;
|