psyclaw 0.27.7 → 0.27.13
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 +21 -12
- package/dist/src/adapters/pi/extension.js +284 -71
- package/dist/src/adapters/pi/extension.js.map +1 -1
- package/dist/src/agents/import.js +2 -1
- package/dist/src/agents/import.js.map +1 -1
- package/dist/src/analysis/hooks.d.ts +19 -2
- package/dist/src/analysis/hooks.js +48 -7
- package/dist/src/analysis/hooks.js.map +1 -1
- package/dist/src/branding.d.ts +2 -2
- package/dist/src/branding.js +2 -2
- package/dist/src/branding.js.map +1 -1
- package/dist/src/chat.js +5 -2
- package/dist/src/chat.js.map +1 -1
- package/dist/src/cli.js +20 -13
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/install/installer.js +3 -0
- package/dist/src/install/installer.js.map +1 -1
- package/dist/src/orchestration/runner.js +1 -0
- package/dist/src/orchestration/runner.js.map +1 -1
- package/dist/src/panel/server.js +16 -10
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/project/approvals.d.ts +50 -0
- package/dist/src/project/approvals.js +62 -0
- package/dist/src/project/approvals.js.map +1 -0
- package/dist/src/project/paths.js +5 -2
- package/dist/src/project/paths.js.map +1 -1
- package/dist/src/research/ledger.d.ts +3 -2
- package/dist/src/research/ledger.js +47 -2
- package/dist/src/research/ledger.js.map +1 -1
- package/dist/src/setup.js +32 -10
- package/dist/src/setup.js.map +1 -1
- package/dist/src/skills/user-skills.d.ts +9 -1
- package/dist/src/skills/user-skills.js +48 -2
- package/dist/src/skills/user-skills.js.map +1 -1
- package/dist/src/style/cli-ui.js +19 -30
- package/dist/src/style/cli-ui.js.map +1 -1
- package/dist/src/tui/provider-picker.d.ts +2 -0
- package/dist/src/tui/provider-picker.js +13 -3
- package/dist/src/tui/provider-picker.js.map +1 -1
- package/dist/src/updates/update.js +13 -20
- package/dist/src/updates/update.js.map +1 -1
- package/dist/src/wizard.js +30 -12
- package/dist/src/wizard.js.map +1 -1
- package/dist/src/workflows/analysis-delegation.js +2 -2
- package/dist/src/workflows/analysis-delegation.js.map +1 -1
- package/package.json +2 -2
- package/scripts/rebrand-pi.mjs +29 -1
- package/skills/core/citation-audit/SKILL.md +9 -9
- package/skills/core/evidence-capture/SKILL.md +9 -9
- package/skills/core/research-brief/SKILL.md +8 -8
- package/skills/core/research-intake/SKILL.md +9 -9
- package/dist/src/telemetry/export.d.ts +0 -58
- package/dist/src/telemetry/export.js +0 -428
- package/dist/src/telemetry/export.js.map +0 -1
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { asProject, bootstrapProject,
|
|
3
|
+
import { appendApproval, approvalInputDigest, approvePrimaryDocument, asProject, bootstrapProject, DEFAULT_RUN_APPROVAL_NODES, PRIMARY_PLAN_DOCUMENTS, primaryPlanApprovalStatus, projectPaths, 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";
|
|
7
7
|
import { readProject } from "../../research/ledger.js";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { lstat, mkdir, readFile } from "node:fs/promises";
|
|
10
|
-
import { PROVIDER_PRESETS, saveProviderConfig } from "../../setup.js";
|
|
10
|
+
import { PROVIDER_PRESETS, providerCredentialSource, saveProviderConfig } from "../../setup.js";
|
|
11
11
|
import { dirname } from "node:path";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
13
|
import { coreSkillNames, enabledRecommendedSkillPaths, normalizeRecommendedSkillId, readRecommendationState, readRecommendedCatalog, recommendedSkillTarget, saveRecommendationState, validateModelInstalledRecommendedSkill, } from "../../skills/recommended.js";
|
|
14
14
|
import { SkillManagerComponent } from "../../tui/skill-manager.js";
|
|
15
|
-
import { enabledLocalSkillPaths, enabledLocalPromptPaths, readUserSkillState, scanLocalSkills, setLocalSkillEnabled, setLocalSkillsEnabled, skillNamesInPaths, userSkillId, } from "../../skills/user-skills.js";
|
|
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
|
-
import {
|
|
17
|
+
import { SecretInputComponent, ProviderPickerComponent, } from "../../tui/provider-picker.js";
|
|
18
18
|
const PARADIGMS = new Set([
|
|
19
19
|
"survey-observational",
|
|
20
20
|
"qualitative-thematic",
|
|
@@ -47,7 +47,17 @@ function parseInitArgs(args) {
|
|
|
47
47
|
async function notifyError(ctx, error) {
|
|
48
48
|
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
49
49
|
}
|
|
50
|
+
async function runPluginCommand(args) {
|
|
51
|
+
const entry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
52
|
+
const modulePath = join(dirname(entry), "package-manager-cli.js");
|
|
53
|
+
const { handlePackageCommand } = await import(pathToFileURL(modulePath).href);
|
|
54
|
+
const handled = await handlePackageCommand(args);
|
|
55
|
+
if (!handled)
|
|
56
|
+
throw new Error(`不支持的 Plugin 操作:${args[0] ?? ""}`);
|
|
57
|
+
}
|
|
50
58
|
const activeAgentRuns = new Set();
|
|
59
|
+
const pendingInitApprovals = new Set();
|
|
60
|
+
const activeApprovalDialogs = new Set();
|
|
51
61
|
const CORE_SKILLS = new Set(["academic-grill", "research-intake", "evidence-capture", "citation-audit", "research-brief"]);
|
|
52
62
|
function coreSkillPath(name) {
|
|
53
63
|
return join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..", "skills", "core", name, "SKILL.md");
|
|
@@ -110,8 +120,14 @@ async function saveDefaultModel(provider, model) {
|
|
|
110
120
|
await atomicWriteFile(path, `${JSON.stringify({ ...settings, defaultProvider: provider, defaultModel: model }, null, 2)}\n`);
|
|
111
121
|
}
|
|
112
122
|
async function pickProviderItem(ctx, title, items) {
|
|
113
|
-
|
|
114
|
-
|
|
123
|
+
if (typeof ctx.ui.custom !== "function") {
|
|
124
|
+
const labels = items.map((item) => `${item.current ? "* " : ""}${item.label} (${item.id})`);
|
|
125
|
+
const selected = await ctx.ui.select(title, labels, { timeout: 60_000 });
|
|
126
|
+
const index = selected === undefined ? -1 : labels.indexOf(selected);
|
|
127
|
+
return index < 0 ? undefined : items[index].id;
|
|
128
|
+
}
|
|
129
|
+
const selected = await ctx.ui.custom((tui, theme, keybindings, done) => new ProviderPickerComponent(title, items, tui, theme, keybindings, done));
|
|
130
|
+
return selected.type === "select" ? selected.id : undefined;
|
|
115
131
|
}
|
|
116
132
|
async function promptProviderKey(ctx, providerName, envName) {
|
|
117
133
|
const result = await ctx.ui.custom((tui, theme, keybindings, done) => new SecretInputComponent(`配置 ${providerName}`, envName, tui, theme, keybindings, done));
|
|
@@ -154,7 +170,7 @@ function academicGrillRequest(subject, mode) {
|
|
|
154
170
|
"从最上游、影响最大的未决问题开始。每一轮只问一个问题,同时给出你的推荐答案或推荐决策及主要取舍。",
|
|
155
171
|
"明确区分探索性与确证性研究,不把规划内容当作已有证据。直到关键分支已经解决或明确交由研究者决定后,再整理研究规格。",
|
|
156
172
|
mode === "init"
|
|
157
|
-
? "持久化模式:init。追问完成后直接更新 academic-grill Skill
|
|
173
|
+
? "持久化模式:init。追问完成后直接更新 academic-grill Skill 规定的全部项目文档,不再询问是否更新;写入后列出实际更新的文件,并把 notes/research-spec.md 与 notes/plan.md 的 Status 统一写为 awaiting-human-approval。"
|
|
158
174
|
: "持久化模式:review。追问完成后先展示拟更新摘要和受影响文件,并询问我是否写回;得到明确确认前不得修改项目文档。",
|
|
159
175
|
].join("\n");
|
|
160
176
|
}
|
|
@@ -165,32 +181,37 @@ async function readControlledRun(root) {
|
|
|
165
181
|
try {
|
|
166
182
|
const value = JSON.parse(await readFile(await controlledRunPath(root), "utf8"));
|
|
167
183
|
return value.schemaVersion === "psyclaw/controlled-run/v1" && value.status === "active" && typeof value.projectId === "string" && typeof value.objective === "string" && typeof value.activatedAt === "string"
|
|
168
|
-
? { ...value, selectedSkills: Array.isArray(value.selectedSkills) ? value.selectedSkills.filter((item) => typeof item === "string") : [] }
|
|
184
|
+
? { ...value, runId: typeof value.runId === "string" ? value.runId : `legacy_${value.projectId}`, mode: value.mode === "auto" ? "auto" : "human", selectedSkills: Array.isArray(value.selectedSkills) ? value.selectedSkills.filter((item) => typeof item === "string") : [] }
|
|
169
185
|
: null;
|
|
170
186
|
}
|
|
171
187
|
catch {
|
|
172
188
|
return null;
|
|
173
189
|
}
|
|
174
190
|
}
|
|
175
|
-
async function activateControlledRun(root, projectId, objective, selectedSkills) {
|
|
191
|
+
async function activateControlledRun(root, runId, projectId, objective, selectedSkills, mode) {
|
|
176
192
|
const state = {
|
|
177
193
|
schemaVersion: "psyclaw/controlled-run/v1",
|
|
194
|
+
runId,
|
|
178
195
|
projectId,
|
|
179
196
|
objective,
|
|
180
197
|
selectedSkills: [...new Set(selectedSkills)],
|
|
198
|
+
mode,
|
|
181
199
|
activatedAt: new Date().toISOString(),
|
|
182
200
|
status: "active",
|
|
183
201
|
};
|
|
184
202
|
await atomicWriteFile(await controlledRunPath(root), `${JSON.stringify(state, null, 2)}\n`);
|
|
185
203
|
return state;
|
|
186
204
|
}
|
|
187
|
-
function controlledRunRequest(objective, selectedSkills) {
|
|
205
|
+
function controlledRunRequest(objective, selectedSkills, mode) {
|
|
188
206
|
return [
|
|
189
|
-
|
|
207
|
+
`PsyClaw ${mode === "auto" ? "自动" : "人在环路"}研究流程已由用户通过 /run 明确启动。先读取 .psyclaw/project.json、.psyclaw/controlled-run.json、.psyclaw/approvals.jsonl、notes/research-spec.md 和 notes/plan.md。`,
|
|
190
208
|
`本次目标:${objective}`,
|
|
191
209
|
selectedSkills.length > 0
|
|
192
210
|
? `本次运行由用户选择的优化 Skill:${selectedSkills.join(", ")}。在相关任务中优先加载并遵循这些 Skill;同名 Skill 只加载一次。`
|
|
193
211
|
: "本次运行未指定额外 Skill,使用 PsyClaw 默认研究流程。不要删除、停用或改写已安装的其他 Skill。",
|
|
212
|
+
mode === "auto"
|
|
213
|
+
? "自动模式:计划内普通步骤连续推进,不再逐项询问;运行时会自动记录审批。原始数据、凭据、破坏性命令、伦理决定、未经核验的事实与外部发布仍必须停止并请求用户决定。"
|
|
214
|
+
: "人在环路模式:每个有副作用或关键方法决策的步骤都必须通过交互审批;拒绝或超时立即停止,不得绕过审批。",
|
|
194
215
|
"实际推进当前尚未完成的研究任务;先形成或更新符合学术规范的 Markdown 分析报告,不能只描述计划。分析报告必须清楚区分数据来源、统计结果、文献证据、限制与尚未核验内容。",
|
|
195
216
|
"分析报告完成后,下一步不是直接询问是否导出 DOCX,而是询问用户是否据此撰写论文。",
|
|
196
217
|
"若用户选择撰写论文,再单独询问是否先进行文献调研,并说明该阶段可能花费较长时间;提示用户可以指定已启用的 Skill,或者选择 PsyClaw 默认的文献调研方式。不要在获得答复前自动开始长时调研。",
|
|
@@ -202,16 +223,92 @@ function controlledRunRequest(objective, selectedSkills) {
|
|
|
202
223
|
}
|
|
203
224
|
function parseRunArgs(args) {
|
|
204
225
|
const value = args.trim();
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
226
|
+
const auto = value === "auto" || value.startsWith("auto ");
|
|
227
|
+
const rest = auto ? value.slice(4).trim() : value;
|
|
228
|
+
if (!rest.startsWith("--skills"))
|
|
229
|
+
return { objective: rest, mode: auto ? "auto" : "human" };
|
|
230
|
+
const match = rest.match(/^--skills(?:=|\s+)([^\s]+)(?:\s+([\s\S]*))?$/);
|
|
208
231
|
if (!match)
|
|
209
|
-
throw new Error("Usage: /run [--skills skill-a,skill-b] [objective]");
|
|
232
|
+
throw new Error("Usage: /run [auto] [--skills skill-a,skill-b] [objective]");
|
|
210
233
|
return {
|
|
211
234
|
objective: (match[2] ?? "").trim(),
|
|
212
235
|
requestedSkills: [...new Set(match[1].split(",").map((item) => item.trim()).filter(Boolean))],
|
|
236
|
+
mode: auto ? "auto" : "human",
|
|
213
237
|
};
|
|
214
238
|
}
|
|
239
|
+
async function planDocumentsReady(root) {
|
|
240
|
+
try {
|
|
241
|
+
const [spec, plan] = await Promise.all([
|
|
242
|
+
readFile(join(root, "notes", "research-spec.md"), "utf8"),
|
|
243
|
+
readFile(join(root, "notes", "plan.md"), "utf8"),
|
|
244
|
+
]);
|
|
245
|
+
return /Status:\s*awaiting-human-approval/i.test(spec) && /Status:\s*awaiting-human-approval/i.test(plan);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function reviewPrimaryDocuments(ctx) {
|
|
252
|
+
if (!ctx.hasUI || activeApprovalDialogs.has(ctx.cwd))
|
|
253
|
+
return false;
|
|
254
|
+
if (!(await planDocumentsReady(ctx.cwd))) {
|
|
255
|
+
ctx.ui.notify("主要计划文档尚未生成完成;请先完成 /init 的学术追问。", "warning");
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
activeApprovalDialogs.add(ctx.cwd);
|
|
259
|
+
try {
|
|
260
|
+
for (const document of PRIMARY_PLAN_DOCUMENTS) {
|
|
261
|
+
const choice = await ctx.ui.select(`审批${document.title}:${document.path}`, ["批准当前版本", "拒绝并返回修改"], { timeout: 120_000 });
|
|
262
|
+
const approved = choice === "批准当前版本";
|
|
263
|
+
await approvePrimaryDocument(ctx.cwd, document, approved ? "approved" : "rejected");
|
|
264
|
+
if (!approved) {
|
|
265
|
+
ctx.ui.notify(`${document.title}未批准;修改后运行 /approve 重新审批。`, "warning");
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
ctx.ui.notify("主要计划文档已批准。文档内容变化后审批会自动失效。", "info");
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
activeApprovalDialogs.delete(ctx.cwd);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function approveRunNodes(ctx, runId, mode) {
|
|
277
|
+
for (const node of DEFAULT_RUN_APPROVAL_NODES) {
|
|
278
|
+
if (mode === "auto") {
|
|
279
|
+
await appendApproval(ctx.cwd, { kind: "step", nodeId: node.id, decision: "auto-approved", actor: "auto", runId, summary: node.summary });
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const options = node.required ? ["批准", "拒绝并停止"] : ["批准", "本次跳过", "拒绝并停止"];
|
|
283
|
+
const choice = await ctx.ui.select(`${node.title}\n${node.summary}`, options, { timeout: 120_000 });
|
|
284
|
+
const decision = choice === "批准" ? "approved" : choice === "本次跳过" ? "skipped" : "rejected";
|
|
285
|
+
await appendApproval(ctx.cwd, { kind: "step", nodeId: node.id, decision, actor: "human", runId, summary: node.summary });
|
|
286
|
+
if (decision === "rejected" || (node.required && decision !== "approved"))
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
function toolApprovalSummary(toolName, input) {
|
|
292
|
+
const record = input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
293
|
+
const target = [record.path, record.file_path, record.command, record.action, record.server, record.tool]
|
|
294
|
+
.find((value) => typeof value === "string");
|
|
295
|
+
return `${toolName}${target ? `: ${target.slice(0, 240)}` : ""}`;
|
|
296
|
+
}
|
|
297
|
+
function toolNeedsApproval(toolName, input) {
|
|
298
|
+
if (["write", "edit", "bash", "powershell", "psyclaw_workbench", "psyclaw_mcp"].includes(toolName))
|
|
299
|
+
return true;
|
|
300
|
+
const action = input && typeof input === "object" && !Array.isArray(input) ? input.action : undefined;
|
|
301
|
+
return typeof action === "string" && /write|save|publish|install|download|execute|call/i.test(action);
|
|
302
|
+
}
|
|
303
|
+
function autoApprovalBlocked(toolName, input) {
|
|
304
|
+
const text = JSON.stringify(input ?? {}).toLocaleLowerCase();
|
|
305
|
+
if (/data[\\/]raw|credential|secret|auth\.json|api[_ -]?key/.test(text))
|
|
306
|
+
return "自动模式不能批准原始数据或凭据路径操作";
|
|
307
|
+
if (["bash", "powershell"].includes(toolName) && /\b(rm|rmdir|del|remove-item|format|shutdown|reboot|git\s+reset\s+--hard)\b/i.test(text)) {
|
|
308
|
+
return "自动模式不能批准破坏性命令";
|
|
309
|
+
}
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
215
312
|
async function selectableRunSkills(root) {
|
|
216
313
|
const state = await readRecommendationState(root);
|
|
217
314
|
const rows = await skillManagerRows(root, state);
|
|
@@ -244,7 +341,7 @@ function resolveRunSkills(requested, available) {
|
|
|
244
341
|
selected.push(name);
|
|
245
342
|
}
|
|
246
343
|
if (unknown.length > 0)
|
|
247
|
-
throw new Error(`本次运行不可用的 Skill:${unknown.join(", ")}。请先通过 /
|
|
344
|
+
throw new Error(`本次运行不可用的 Skill:${unknown.join(", ")}。请先通过 /skill 安装或启用。`);
|
|
248
345
|
return selected;
|
|
249
346
|
}
|
|
250
347
|
async function recommendedItems(kind) {
|
|
@@ -597,12 +694,12 @@ function modelSkillInstallTask(root, row, scope) {
|
|
|
597
694
|
...(row.installHint ? [`仓库入口提示:${row.installHint}`] : []),
|
|
598
695
|
`安装位置:${skillScopeLabel(scope)}。`,
|
|
599
696
|
`唯一允许的最终目标目录:${target}`,
|
|
600
|
-
"用户已通过 Skill 管理页授权本次安装。请使用当前会话的联网、文件和命令工具读取来源仓库,并直接下载、安装所需依赖和完成 Skill 安装;不要再次要求安装权限。不要写入其他 Skill 目录,不要修改 data/raw、.git 或研究产物。",
|
|
697
|
+
"用户已通过 Skill 管理页授权本次安装。请使用当前会话的联网、文件和命令工具读取来源仓库,并直接下载、安装所需依赖和完成 Skill 安装;不要再次要求安装权限。不要写入其他 Skill 目录,不要修改 .psyclaw/data/raw、data/raw、.git 或研究产物。",
|
|
601
698
|
row.collection
|
|
602
699
|
? "这是多 Skill 套件:目标目录自身无需 SKILL.md,但其子目录必须包含一个或多个有效 SKILL.md。保留套件内共享目录和相对路径,不得包含 .git、符号链接或凭据。"
|
|
603
700
|
: "目标目录最终必须直接包含有效 SKILL.md(YAML frontmatter 至少包含 name 和 description),不得包含 .git、符号链接、凭据或二进制大文件。",
|
|
604
701
|
"如果仓库包含多个 Skill,只安装与此推荐项相符的部分;如果它不是 Skill 或无法合理适配,停止并说明原因,不要伪造 SKILL.md。",
|
|
605
|
-
"安装完成后检查目标目录结构,并提醒用户执行 /
|
|
702
|
+
"安装完成后检查目标目录结构,并提醒用户执行 /skill 启用该项,再执行 /reload。",
|
|
606
703
|
].join("\n");
|
|
607
704
|
}
|
|
608
705
|
async function chooseSkillScope(ctx) {
|
|
@@ -628,17 +725,17 @@ async function queueModelSkillInstall(pi, ctx, row) {
|
|
|
628
725
|
state.skillScopes = { ...(state.skillScopes ?? {}), [row.id]: scope };
|
|
629
726
|
await saveRecommendationState(ctx.cwd, state);
|
|
630
727
|
pi.sendUserMessage(modelSkillInstallTask(ctx.cwd, row, scope), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
631
|
-
ctx.ui.notify(`已将 ${row.name} 的安装任务交给当前模型,目标为${skillScopeLabel(scope)}。安装完成后请在 /
|
|
728
|
+
ctx.ui.notify(`已将 ${row.name} 的安装任务交给当前模型,目标为${skillScopeLabel(scope)}。安装完成后请在 /skill 中启用,再执行 /reload。`, "info");
|
|
632
729
|
}
|
|
633
730
|
async function showSkillManager(pi, args, ctx) {
|
|
634
731
|
const action = args.trim().split(/\s+/).filter(Boolean);
|
|
635
732
|
const verb = action[0];
|
|
636
733
|
const id = action[1];
|
|
637
734
|
if (verb && !["status", "enable", "disable", "enable-all", "disable-all", "install"].includes(verb)) {
|
|
638
|
-
throw new Error("Usage: /
|
|
735
|
+
throw new Error("Usage: /skill [status|enable <id>|disable <id>|enable-all|disable-all]");
|
|
639
736
|
}
|
|
640
737
|
if ((verb === "enable" || verb === "disable" || verb === "install") && !id) {
|
|
641
|
-
throw new Error(`Usage: /
|
|
738
|
+
throw new Error(`Usage: /skill ${verb} <id>`);
|
|
642
739
|
}
|
|
643
740
|
if (verb === "install") {
|
|
644
741
|
const row = (await skillManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd))).find((candidate) => candidate.id === normalizeRecommendedSkillId(id));
|
|
@@ -858,17 +955,14 @@ export default function psyclawExtension(pi) {
|
|
|
858
955
|
const legacyTestApi = typeof pi.registerTool !== "function";
|
|
859
956
|
const runtimeMcps = new RuntimeMcpRegistry();
|
|
860
957
|
if (!legacyTestApi && typeof pi.on === "function")
|
|
861
|
-
pi.on("resources_discover", async (event
|
|
958
|
+
pi.on("resources_discover", async (event) => {
|
|
862
959
|
const enabled = await enabledRecommendedSkillPaths(event.cwd);
|
|
863
|
-
for (const warning of enabled.warnings)
|
|
864
|
-
ctx.ui.notify(`PsyClaw Skill: ${warning}`, "warning");
|
|
865
960
|
const local = await enabledLocalSkillPaths(event.cwd, {
|
|
866
961
|
excludedNames: [...coreSkillNames(), ...await skillNamesInPaths(enabled.paths)],
|
|
867
962
|
});
|
|
868
|
-
for (const warning of local.warnings)
|
|
869
|
-
ctx.ui.notify(`PsyClaw Skill: ${warning}`, "warning");
|
|
870
963
|
// Return only existing Skill directories. Same-name local/core candidates
|
|
871
|
-
// are resolved before Pi sees them
|
|
964
|
+
// are resolved before Pi sees them. Discovery warnings belong in /skill;
|
|
965
|
+
// they should not interrupt every startup.
|
|
872
966
|
return {
|
|
873
967
|
skillPaths: [...enabled.paths, ...local.paths],
|
|
874
968
|
promptPaths: await enabledLocalPromptPaths(event.cwd),
|
|
@@ -876,6 +970,47 @@ export default function psyclawExtension(pi) {
|
|
|
876
970
|
});
|
|
877
971
|
if (!legacyTestApi && typeof pi.on === "function")
|
|
878
972
|
pi.on("session_shutdown", () => runtimeMcps.close());
|
|
973
|
+
if (!legacyTestApi && typeof pi.on === "function")
|
|
974
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
975
|
+
const run = await readControlledRun(ctx.cwd);
|
|
976
|
+
if (!run || !toolNeedsApproval(event.toolName, event.input))
|
|
977
|
+
return;
|
|
978
|
+
const summary = toolApprovalSummary(event.toolName, event.input);
|
|
979
|
+
if (run.mode === "auto") {
|
|
980
|
+
const blockedReason = autoApprovalBlocked(event.toolName, event.input);
|
|
981
|
+
await appendApproval(ctx.cwd, {
|
|
982
|
+
kind: "tool",
|
|
983
|
+
nodeId: event.toolCallId,
|
|
984
|
+
decision: blockedReason ? "rejected" : "auto-approved",
|
|
985
|
+
actor: "auto",
|
|
986
|
+
runId: run.runId,
|
|
987
|
+
sha256: approvalInputDigest({ toolName: event.toolName, input: event.input }),
|
|
988
|
+
summary: blockedReason ?? summary,
|
|
989
|
+
});
|
|
990
|
+
return blockedReason ? { block: true, terminate: true, reason: blockedReason } : undefined;
|
|
991
|
+
}
|
|
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
|
+
await appendApproval(ctx.cwd, {
|
|
997
|
+
kind: "tool",
|
|
998
|
+
nodeId: event.toolCallId,
|
|
999
|
+
decision: approved ? "approved" : "rejected",
|
|
1000
|
+
actor: "human",
|
|
1001
|
+
runId: run.runId,
|
|
1002
|
+
sha256: approvalInputDigest({ toolName: event.toolName, input: event.input }),
|
|
1003
|
+
summary,
|
|
1004
|
+
});
|
|
1005
|
+
return approved ? undefined : { block: true, terminate: true, reason: "用户拒绝该执行步骤或审批超时" };
|
|
1006
|
+
});
|
|
1007
|
+
if (!legacyTestApi && typeof pi.on === "function")
|
|
1008
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
1009
|
+
if (!pendingInitApprovals.has(ctx.cwd) || !(await planDocumentsReady(ctx.cwd)))
|
|
1010
|
+
return;
|
|
1011
|
+
pendingInitApprovals.delete(ctx.cwd);
|
|
1012
|
+
await reviewPrimaryDocuments(ctx);
|
|
1013
|
+
});
|
|
879
1014
|
pi.registerCommand("init", {
|
|
880
1015
|
description: "初始化可追溯的研究项目",
|
|
881
1016
|
handler: async (args, ctx) => {
|
|
@@ -883,8 +1018,9 @@ export default function psyclawExtension(pi) {
|
|
|
883
1018
|
const parsed = parseInitArgs(args);
|
|
884
1019
|
const project = await bootstrapProject({ root: ctx.cwd, ...parsed });
|
|
885
1020
|
if (typeof pi.sendUserMessage === "function") {
|
|
1021
|
+
pendingInitApprovals.add(ctx.cwd);
|
|
886
1022
|
pi.sendUserMessage(academicGrillRequest(project.goal, "init"), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
887
|
-
ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm}
|
|
1023
|
+
ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm})。学术追问完成并生成主要计划文档后会进入审批;全部批准后才能 /run。`, "info");
|
|
888
1024
|
}
|
|
889
1025
|
else {
|
|
890
1026
|
ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm})。使用 /run 启动受控研究流程。`, "info");
|
|
@@ -895,6 +1031,18 @@ export default function psyclawExtension(pi) {
|
|
|
895
1031
|
}
|
|
896
1032
|
},
|
|
897
1033
|
});
|
|
1034
|
+
if (!legacyTestApi)
|
|
1035
|
+
pi.registerCommand("approve", {
|
|
1036
|
+
description: "审批 /init 生成的主要研究计划文档",
|
|
1037
|
+
handler: async (_args, ctx) => {
|
|
1038
|
+
try {
|
|
1039
|
+
await reviewPrimaryDocuments(ctx);
|
|
1040
|
+
}
|
|
1041
|
+
catch (error) {
|
|
1042
|
+
await notifyError(ctx, error);
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
1045
|
+
});
|
|
898
1046
|
if (!legacyTestApi)
|
|
899
1047
|
pi.registerCommand("run", {
|
|
900
1048
|
description: "启动受控研究流程,可用 --skills a,b 选择本次优化 Skill",
|
|
@@ -919,8 +1067,16 @@ export default function psyclawExtension(pi) {
|
|
|
919
1067
|
}
|
|
920
1068
|
ctx.ui.notify("检测到合规的分析文档,已自动补齐研究项目记录并启动受控流程。", "info");
|
|
921
1069
|
}
|
|
1070
|
+
const planApproval = await primaryPlanApprovalStatus(ctx.cwd);
|
|
1071
|
+
if (!planApproval.ok) {
|
|
1072
|
+
const missing = planApproval.documents.filter((item) => !item.approved).map((item) => item.path);
|
|
1073
|
+
ctx.ui.notify(`主要计划尚未批准或批准后已修改:${missing.join("、")}。请运行 /approve。`, "warning");
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
922
1076
|
const available = await selectableRunSkills(ctx.cwd);
|
|
923
1077
|
let requestedSkills = parsed.requestedSkills;
|
|
1078
|
+
if (parsed.mode === "auto" && requestedSkills === undefined)
|
|
1079
|
+
requestedSkills = [];
|
|
924
1080
|
if (requestedSkills === undefined && ctx.hasUI && typeof ctx.ui.input === "function") {
|
|
925
1081
|
const answer = await ctx.ui.input("选择本次 Run 使用的 Skill(逗号分隔,可留空)", available.map((skill) => skill.name).join(", "));
|
|
926
1082
|
if (answer === undefined) {
|
|
@@ -931,10 +1087,24 @@ export default function psyclawExtension(pi) {
|
|
|
931
1087
|
}
|
|
932
1088
|
const selectedSkills = resolveRunSkills(requestedSkills ?? [], available);
|
|
933
1089
|
const objective = parsed.objective || project.goal;
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1090
|
+
const runId = `run_${Date.now()}`;
|
|
1091
|
+
await appendApproval(ctx.cwd, {
|
|
1092
|
+
kind: "run-mode",
|
|
1093
|
+
nodeId: "run-mode",
|
|
1094
|
+
decision: parsed.mode === "auto" ? "auto-approved" : "approved",
|
|
1095
|
+
actor: parsed.mode === "auto" ? "auto" : "human",
|
|
1096
|
+
runId,
|
|
1097
|
+
sha256: approvalInputDigest({ projectId: project.id, objective, selectedSkills, mode: parsed.mode }),
|
|
1098
|
+
summary: parsed.mode === "auto" ? "用户选择自动运行模式" : "用户选择人在环路模式",
|
|
1099
|
+
});
|
|
1100
|
+
if (!(await approveRunNodes(ctx, runId, parsed.mode))) {
|
|
1101
|
+
ctx.ui.notify("运行审批被拒绝或超时,未启动。", "warning");
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
await activateControlledRun(ctx.cwd, runId, project.id, objective, selectedSkills, parsed.mode);
|
|
1105
|
+
pi.appendEntry("psyclaw:controlled-run", { runId, projectId: project.id, objective, selectedSkills, mode: parsed.mode, activatedAt: new Date().toISOString() });
|
|
1106
|
+
pi.sendUserMessage(controlledRunRequest(objective, selectedSkills, parsed.mode), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
1107
|
+
ctx.ui.notify(`${parsed.mode === "auto" ? "自动" : "人在环路"}研究流程已启动${selectedSkills.length > 0 ? `;本次使用 Skill:${selectedSkills.join(", ")}` : ";使用默认 Skill"}。`, "info");
|
|
938
1108
|
}
|
|
939
1109
|
catch (error) {
|
|
940
1110
|
await notifyError(ctx, error);
|
|
@@ -1072,27 +1242,73 @@ export default function psyclawExtension(pi) {
|
|
|
1072
1242
|
},
|
|
1073
1243
|
});
|
|
1074
1244
|
if (!legacyTestApi)
|
|
1075
|
-
pi.registerCommand("
|
|
1076
|
-
description: "
|
|
1245
|
+
pi.registerCommand("skill", {
|
|
1246
|
+
description: "管理和安装 Skill",
|
|
1077
1247
|
handler: async (args, ctx) => {
|
|
1078
|
-
|
|
1079
|
-
|
|
1248
|
+
const [name, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
1249
|
+
if (!name) {
|
|
1250
|
+
try {
|
|
1251
|
+
await showSkillManager(pi, "", ctx);
|
|
1252
|
+
}
|
|
1253
|
+
catch (error) {
|
|
1254
|
+
await notifyError(ctx, error);
|
|
1255
|
+
}
|
|
1256
|
+
return;
|
|
1080
1257
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1258
|
+
if (name === "install") {
|
|
1259
|
+
const source = rest.join(" ").trim();
|
|
1260
|
+
if (!source) {
|
|
1261
|
+
ctx.ui.notify("Usage: /skill install <local-directory>", "info");
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
try {
|
|
1265
|
+
const installed = await installLocalSkill(source, ctx.cwd);
|
|
1266
|
+
ctx.ui.notify(`已安装本地 Skill ${installed.name}:${installed.target}。请执行 /reload。`, "info");
|
|
1267
|
+
}
|
|
1268
|
+
catch (error) {
|
|
1269
|
+
await notifyError(ctx, error);
|
|
1270
|
+
}
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (["status", "enable", "disable", "enable-all", "disable-all"].includes(name)) {
|
|
1274
|
+
try {
|
|
1275
|
+
await showSkillManager(pi, args, ctx);
|
|
1276
|
+
}
|
|
1277
|
+
catch (error) {
|
|
1278
|
+
await notifyError(ctx, error);
|
|
1279
|
+
}
|
|
1280
|
+
return;
|
|
1083
1281
|
}
|
|
1282
|
+
ctx.ui.notify("Usage: /skill [status|enable <id>|disable <id>|enable-all|disable-all|install <local-directory>]", "info");
|
|
1084
1283
|
},
|
|
1085
1284
|
});
|
|
1086
1285
|
if (!legacyTestApi)
|
|
1087
|
-
pi.registerCommand("
|
|
1088
|
-
description: "
|
|
1286
|
+
pi.registerCommand("plugin", {
|
|
1287
|
+
description: "管理 Plugin / Extension",
|
|
1089
1288
|
handler: async (args, ctx) => {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1289
|
+
try {
|
|
1290
|
+
const [action, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
1291
|
+
if (!action) {
|
|
1292
|
+
ctx.ui.notify("Usage: /plugin list | install <source> [-l] | remove <source> [-l]", "info");
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
if (!["list", "install", "remove"].includes(action)) {
|
|
1296
|
+
throw new Error("Usage: /plugin list | install <source> [-l] | remove <source> [-l]");
|
|
1297
|
+
}
|
|
1298
|
+
if (action !== "list" && rest.length === 0)
|
|
1299
|
+
throw new Error(`Usage: /plugin ${action} <source> [-l]`);
|
|
1300
|
+
if (action !== "list") {
|
|
1301
|
+
const approved = await ctx.ui.confirm(`${action === "install" ? "安装" : "移除"} Plugin?`, `${rest.join(" ")}\nPlugin 与宿主进程同权限,操作完成后需要重启 PsyClaw。`);
|
|
1302
|
+
if (!approved)
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
await runPluginCommand([action, ...rest]);
|
|
1306
|
+
if (action !== "list")
|
|
1307
|
+
ctx.ui.notify("Plugin 配置已更新,请重启 PsyClaw。", "info");
|
|
1308
|
+
}
|
|
1309
|
+
catch (error) {
|
|
1310
|
+
await notifyError(ctx, error);
|
|
1094
1311
|
}
|
|
1095
|
-
pi.sendUserMessage(`/skill:${name}${rest.length ? ` ${rest.join(" ")}` : ""}`, ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
1096
1312
|
},
|
|
1097
1313
|
});
|
|
1098
1314
|
if (!legacyTestApi)
|
|
@@ -1118,7 +1334,7 @@ export default function psyclawExtension(pi) {
|
|
|
1118
1334
|
const skillLines = skills.items.slice(0, 8).map((item) => `Skill: ${String(item.id)} — ${String(item.name)}`);
|
|
1119
1335
|
const mcpLines = mcps.items.slice(0, 8).map((item) => `MCP: ${String(item.id)} — ${String(item.name)}`);
|
|
1120
1336
|
const toolLines = skills.externalTools.slice(0, 8).map((item) => `外部工具: ${String(item.name)} — ${String(item.sourceRef ?? "请查看项目文档")}`);
|
|
1121
|
-
ctx.ui.notify(["推荐安装入口", "", ...skillLines, ...mcpLines, "", ...toolLines, "", "安装:/install skill|mcp <id>", "管理:/
|
|
1337
|
+
ctx.ui.notify(["推荐安装入口", "", ...skillLines, ...mcpLines, "", ...toolLines, "", "安装:/install skill|mcp <id>", "管理:/skill 或 /mcp", "外部工具不通过 Skill 安装器安装", "也可以打开 /panel 查看推荐页面"].join("\n"), "info");
|
|
1122
1338
|
return;
|
|
1123
1339
|
}
|
|
1124
1340
|
if (kind !== "skill" && kind !== "mcp")
|
|
@@ -1202,11 +1418,26 @@ export default function psyclawExtension(pi) {
|
|
|
1202
1418
|
selectedId = choice;
|
|
1203
1419
|
}
|
|
1204
1420
|
if (preset && ctx.hasUI && typeof ctx.ui.custom === "function") {
|
|
1421
|
+
const credential = await providerCredentialSource(preset);
|
|
1422
|
+
// Always show the credential step when switching a built-in
|
|
1423
|
+
// provider. An empty submission deliberately keeps an existing
|
|
1424
|
+
// environment/auth-store credential; this makes the flow explicit
|
|
1425
|
+
// and avoids silently skipping the key screen on another machine.
|
|
1205
1426
|
const key = await promptProviderKey(ctx, preset.name, preset.apiKeyEnv);
|
|
1206
1427
|
if (key === undefined)
|
|
1207
1428
|
return;
|
|
1429
|
+
if (!key && credential === "missing") {
|
|
1430
|
+
throw new Error(`未找到 ${preset.apiKeyEnv};请输入 API Key 后再继续`);
|
|
1431
|
+
}
|
|
1208
1432
|
await saveProviderConfig({ ...preset, ...(key ? { apiKey: key } : {}) });
|
|
1209
|
-
await
|
|
1433
|
+
const refreshed = await Promise.race([
|
|
1434
|
+
ctx.modelRegistry.refresh().then(() => true),
|
|
1435
|
+
new Promise((resolve) => setTimeout(() => resolve(false), 5_000)),
|
|
1436
|
+
]);
|
|
1437
|
+
if (!refreshed) {
|
|
1438
|
+
ctx.ui.notify("Provider 配置已保存;模型目录刷新超时,请重新启动 PsyClaw 后使用。", "warning");
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1210
1441
|
models = ctx.modelRegistry.getAll().filter((model) => model.provider === requested);
|
|
1211
1442
|
}
|
|
1212
1443
|
const selected = models.find((model) => model.id === selectedId);
|
|
@@ -1240,27 +1471,6 @@ export default function psyclawExtension(pi) {
|
|
|
1240
1471
|
ctx.ui.notify(`启动横幅宠物已${action === "on" ? "开启" : "关闭"},下次启动生效`, "info");
|
|
1241
1472
|
},
|
|
1242
1473
|
});
|
|
1243
|
-
const traceCommand = {
|
|
1244
|
-
description: "导出包含正文与工具参数的完整使用路径,供 Langfuse 或 LangSmith 分析",
|
|
1245
|
-
handler: async (args, ctx) => {
|
|
1246
|
-
try {
|
|
1247
|
-
if (args.trim())
|
|
1248
|
-
throw new Error("Usage: /export");
|
|
1249
|
-
const result = await exportTraces({ root: ctx.cwd });
|
|
1250
|
-
ctx.ui.notify([
|
|
1251
|
-
"使用路径已导出(未上传)",
|
|
1252
|
-
`文件:${result.output}`,
|
|
1253
|
-
`轨迹:${result.traces}`,
|
|
1254
|
-
`步骤:${result.spans}`,
|
|
1255
|
-
"包含对话正文、工具参数、原始 ID 与绝对路径,便于排查各环节问题;请勿将导出文件提交到公开仓库。",
|
|
1256
|
-
].join("\n"), "info");
|
|
1257
|
-
}
|
|
1258
|
-
catch (error) {
|
|
1259
|
-
await notifyError(ctx, error);
|
|
1260
|
-
}
|
|
1261
|
-
},
|
|
1262
|
-
};
|
|
1263
|
-
pi.registerCommand("export", traceCommand);
|
|
1264
1474
|
if (typeof pi.registerTool === "function") {
|
|
1265
1475
|
pi.registerTool({
|
|
1266
1476
|
name: "psyclaw_mcp",
|
|
@@ -1529,7 +1739,10 @@ export default function psyclawExtension(pi) {
|
|
|
1529
1739
|
}
|
|
1530
1740
|
},
|
|
1531
1741
|
});
|
|
1532
|
-
|
|
1742
|
+
// Read-only multi-agent research is a user-facing capability. Keep the
|
|
1743
|
+
// destructive/developer commands gated, but do not hide the bounded agent
|
|
1744
|
+
// runner behind PSYCLAW_DEVELOPER_COMMANDS in published builds.
|
|
1745
|
+
if (!legacyTestApi)
|
|
1533
1746
|
pi.registerCommand("agents", {
|
|
1534
1747
|
description: "运行经批准的只读研究 Agent",
|
|
1535
1748
|
handler: async (args, ctx) => {
|
|
@@ -1556,7 +1769,7 @@ export default function psyclawExtension(pi) {
|
|
|
1556
1769
|
ctx.ui.notify("请先使用 /init 初始化研究项目,再运行 /agents", "warning");
|
|
1557
1770
|
return;
|
|
1558
1771
|
}
|
|
1559
|
-
const approved = await ctx.ui.confirm("Run read-only research worker?", "The worker runs in a separate
|
|
1772
|
+
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.");
|
|
1560
1773
|
if (!approved) {
|
|
1561
1774
|
ctx.ui.notify("Agent run canceled", "info");
|
|
1562
1775
|
return;
|