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