psyclaw 0.27.0 → 0.27.5
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 +9 -5
- package/dist/apps/panel/index.html +78 -3406
- package/dist/src/adapters/pi/extension.js +406 -107
- package/dist/src/adapters/pi/extension.js.map +1 -1
- package/dist/src/adapters/pi/rpc.js +7 -4
- package/dist/src/adapters/pi/rpc.js.map +1 -1
- package/dist/src/branding.js +7 -5
- package/dist/src/branding.js.map +1 -1
- package/dist/src/chat.js +0 -5
- package/dist/src/chat.js.map +1 -1
- package/dist/src/cli.js +0 -0
- package/dist/src/core/citations.d.ts +2 -0
- package/dist/src/core/citations.js +4 -1
- package/dist/src/core/citations.js.map +1 -1
- package/dist/src/core/docx.js +2 -2
- package/dist/src/core/docx.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/literature/archive.d.ts +30 -0
- package/dist/src/literature/archive.js +133 -0
- package/dist/src/literature/archive.js.map +1 -0
- package/dist/src/orchestration/research-agents.d.ts +32 -0
- package/dist/src/orchestration/research-agents.js +225 -0
- package/dist/src/orchestration/research-agents.js.map +1 -0
- package/dist/src/panel/extension.js +2 -52
- package/dist/src/panel/extension.js.map +1 -1
- package/dist/src/panel/server.d.ts +2 -0
- package/dist/src/panel/server.js +113 -74
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/project/hitl.js +2 -0
- package/dist/src/project/hitl.js.map +1 -1
- package/dist/src/project/paths.d.ts +2 -6
- package/dist/src/project/paths.js +4 -12
- package/dist/src/project/paths.js.map +1 -1
- package/dist/src/skills/recommended.d.ts +10 -0
- package/dist/src/skills/recommended.js +85 -7
- package/dist/src/skills/recommended.js.map +1 -1
- package/dist/src/telemetry/export.d.ts +32 -0
- package/dist/src/telemetry/export.js +102 -1
- package/dist/src/telemetry/export.js.map +1 -1
- package/dist/src/tui/provider-picker.d.ts +48 -0
- package/dist/src/tui/provider-picker.js +115 -0
- package/dist/src/tui/provider-picker.js.map +1 -0
- package/dist/src/workflows/academic-export.d.ts +14 -0
- package/dist/src/workflows/academic-export.js +42 -0
- package/dist/src/workflows/academic-export.js.map +1 -0
- package/dist/src/workflows/meta-analysis.d.ts +1 -0
- package/dist/src/workflows/meta-analysis.js +17 -9
- package/dist/src/workflows/meta-analysis.js.map +1 -1
- package/dist/src/workflows/publish.js +14 -0
- package/dist/src/workflows/publish.js.map +1 -1
- package/package.json +1 -1
- package/skills/core/academic-grill/SKILL.md +77 -0
- package/skills/core/manifest.json +3 -2
- package/skills/recommended/catalog.json +210 -44
package/dist/src/panel/server.js
CHANGED
|
@@ -13,7 +13,6 @@ import { KNOWN_AGENTS } from "../agents/catalog.js";
|
|
|
13
13
|
import { planAgentInstall } from "../install/installer.js";
|
|
14
14
|
import { deepSeekProviderSpec, PiModelGateway } from "../adapters/pi/model.js";
|
|
15
15
|
import { PROVIDER_PRESETS, saveProviderConfig } from "../setup.js";
|
|
16
|
-
import { readHitlWorkspace } from "../project/hitl.js";
|
|
17
16
|
import { assertSafeProjectPath, projectPaths } from "../project/paths.js";
|
|
18
17
|
import { readManuscript } from "../project/manuscript.js";
|
|
19
18
|
import { appendJsonlIfMissing, atomicWriteFile, readJsonl } from "../project/jsonl.js";
|
|
@@ -30,11 +29,16 @@ export { verifyDoi } from "../core/doi.js";
|
|
|
30
29
|
import { resumePlanWithPi } from "../orchestration/pi-executor.js";
|
|
31
30
|
import { RunEventLog } from "./events.js";
|
|
32
31
|
import { PSYCLAW_IDENTITY_PROMPT } from "../branding.js";
|
|
32
|
+
import { recommendedSkillTarget } from "../skills/recommended.js";
|
|
33
|
+
import { projectTraceSnapshot } from "../telemetry/export.js";
|
|
33
34
|
const activePanelRuns = new Set();
|
|
34
35
|
async function readRecommendationState(root) {
|
|
35
36
|
try {
|
|
36
37
|
const value = JSON.parse(await readFile(join(root, ".psyclaw", "recommendations.json"), "utf8"));
|
|
37
|
-
|
|
38
|
+
const skillScopes = value.skillScopes && typeof value.skillScopes === "object"
|
|
39
|
+
? Object.fromEntries(Object.entries(value.skillScopes).filter((entry) => entry[1] === "project" || entry[1] === "user"))
|
|
40
|
+
: undefined;
|
|
41
|
+
return { schemaVersion: "psyclaw/recommendation-state/v1", skills: Array.isArray(value.skills) ? value.skills.filter((id) => typeof id === "string") : [], mcp: Array.isArray(value.mcp) ? value.mcp.filter((id) => typeof id === "string") : [], ...(skillScopes === undefined ? {} : { skillScopes }) };
|
|
38
42
|
}
|
|
39
43
|
catch {
|
|
40
44
|
return { schemaVersion: "psyclaw/recommendation-state/v1", skills: [], mcp: [] };
|
|
@@ -45,6 +49,7 @@ async function writeRecommendationState(root, state) {
|
|
|
45
49
|
}
|
|
46
50
|
/** The bundled core skills, always listed so the user can disable (not uninstall) them. */
|
|
47
51
|
const CORE_SKILLS = [
|
|
52
|
+
{ id: "academic-grill", name: "学术追问" },
|
|
48
53
|
{ id: "research-intake", name: "研究入口" },
|
|
49
54
|
{ id: "evidence-capture", name: "证据登记" },
|
|
50
55
|
{ id: "citation-audit", name: "引用审计" },
|
|
@@ -154,7 +159,7 @@ async function appendLedgerEvidence(root, body) {
|
|
|
154
159
|
* Any root-level document (questionnaire exports etc.) is listed as well.
|
|
155
160
|
*/
|
|
156
161
|
const DOCUMENT_EXTENSIONS = new Set(["md", "markdown", "docx", "doc", "pdf", "xlsx", "xls", "csv", "txt"]);
|
|
157
|
-
const DOCUMENT_SKIP_DIRS = [".psyclaw", ".git", "node_modules", "analysis/scripts", "analysis/results", "analysis/configs", "
|
|
162
|
+
const DOCUMENT_SKIP_DIRS = [".psyclaw", ".git", "node_modules", "analysis/scripts", "analysis/results", "analysis/configs", "logs", "paper/archive", "literature/pdfs"];
|
|
158
163
|
/** Classify a document by its location and extension (shared by scan + import). */
|
|
159
164
|
function classifyDocumentKind(relative, extension) {
|
|
160
165
|
return relative.startsWith("paper/") || relative.startsWith("docs/")
|
|
@@ -387,12 +392,55 @@ async function panelStats(root) {
|
|
|
387
392
|
const files = await listProjectFiles(root);
|
|
388
393
|
return { schemaVersion: "psyclaw/panel-stats/v1", runs: (await listRuns(root)).length, evidence: await countLines(paths.evidence), claims: await countLines(paths.claims), auditEvents: await countLines(paths.audit), trackedFiles: files.length, outputs: files.filter((path) => path.startsWith("outputs/")).length, generatedAt: new Date().toISOString() };
|
|
389
394
|
}
|
|
395
|
+
const PANEL_FILE_ROOTS = ["notes/", "paper/", "docs/", "outputs/", "logs/"];
|
|
396
|
+
const PANEL_FILE_EXTENSIONS = /\.(md|markdown|txt|json|csv)$/i;
|
|
397
|
+
function panelFileAllowed(relative) {
|
|
398
|
+
const normalized = relative.replaceAll("\\", "/");
|
|
399
|
+
return PANEL_FILE_ROOTS.some((prefix) => normalized.startsWith(prefix))
|
|
400
|
+
&& PANEL_FILE_EXTENSIONS.test(normalized)
|
|
401
|
+
&& !/credential|secret|auth\.json|\.env/i.test(normalized);
|
|
402
|
+
}
|
|
403
|
+
async function panelProjectFiles(root) {
|
|
404
|
+
return (await listProjectFiles(root)).filter(panelFileAllowed);
|
|
405
|
+
}
|
|
406
|
+
async function readPanelProjectFile(root, relative) {
|
|
407
|
+
const normalized = relative.replaceAll("\\", "/").trim();
|
|
408
|
+
if (!panelFileAllowed(normalized) || normalized.split("/").some((part) => part === "" || part === "..")) {
|
|
409
|
+
throw new Error("project file is outside the Panel read-only allowlist");
|
|
410
|
+
}
|
|
411
|
+
const target = await assertSafeProjectPath(root, normalized);
|
|
412
|
+
const stat = await lstat(target);
|
|
413
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
414
|
+
throw new Error("project file must be a regular file");
|
|
415
|
+
if (stat.size > 2 * 1024 * 1024)
|
|
416
|
+
throw new Error("project file is too large for Panel preview");
|
|
417
|
+
const format = normalized.toLowerCase().endsWith(".json") ? "json"
|
|
418
|
+
: normalized.toLowerCase().endsWith(".csv") ? "csv"
|
|
419
|
+
: /\.(md|markdown)$/i.test(normalized) ? "markdown" : "text";
|
|
420
|
+
return { path: normalized, content: await readFile(target, "utf8"), format };
|
|
421
|
+
}
|
|
390
422
|
function publicInstallPlan(plan) {
|
|
391
423
|
// `projectRoot` is an internal containment anchor. It is not needed by a
|
|
392
424
|
// read-only browser and would disclose a local filesystem path.
|
|
393
425
|
const { projectRoot: _projectRoot, ...publicFields } = plan;
|
|
394
426
|
return publicFields;
|
|
395
427
|
}
|
|
428
|
+
function panelSkillInstallTask(root, item, scope) {
|
|
429
|
+
const id = String(item.id ?? "");
|
|
430
|
+
const name = String(item.name ?? id);
|
|
431
|
+
const sourceRef = String(item.sourceRef ?? "");
|
|
432
|
+
const target = recommendedSkillTarget(root, id, scope);
|
|
433
|
+
return [
|
|
434
|
+
`安装推荐 Skill:${name} (${id})。`,
|
|
435
|
+
`来源网址:${sourceRef}`,
|
|
436
|
+
`安装位置:${scope === "user" ? "系统目录(所有项目)" : "项目目录(仅当前项目)"}。`,
|
|
437
|
+
`唯一允许的最终目标目录:${target}`,
|
|
438
|
+
"请使用当前会话的工具检查来源仓库并完成安装。不要写入其他 Skill 目录,不要修改 data/raw、.git 或研究产物。",
|
|
439
|
+
"目标目录最终必须直接包含有效 SKILL.md(YAML frontmatter 至少包含 name 和 description),不得包含 .git、符号链接、凭据或二进制大文件。",
|
|
440
|
+
"如果仓库包含多个 Skill,只安装与此推荐项相符的部分;如果它不是 Skill 或无法合理适配,停止并说明原因,不要伪造 SKILL.md。",
|
|
441
|
+
"安装完成后检查目标目录结构,并提醒用户执行 /skills 启用该项,再执行 /reload。",
|
|
442
|
+
].join("\n");
|
|
443
|
+
}
|
|
396
444
|
/** Metadata-only catalog for the optional panel. No install or credential read. */
|
|
397
445
|
async function panelCatalog(root) {
|
|
398
446
|
const scans = await discoverAgents();
|
|
@@ -532,7 +580,15 @@ async function recommendedSkills() {
|
|
|
532
580
|
for (const path of candidates) {
|
|
533
581
|
try {
|
|
534
582
|
const catalog = JSON.parse(await readFile(path, "utf8"));
|
|
535
|
-
return {
|
|
583
|
+
return {
|
|
584
|
+
...catalog,
|
|
585
|
+
items: (catalog.items ?? []).filter((item) => item.kind === "skill").map((item) => ({
|
|
586
|
+
...item,
|
|
587
|
+
slashCommand: `/skills enable ${String(item.id ?? "")}`,
|
|
588
|
+
installCommand: `/install skill ${String(item.id ?? "")}`,
|
|
589
|
+
})),
|
|
590
|
+
externalTools: (catalog.externalTools ?? []).filter((item) => item.kind === "external-tool"),
|
|
591
|
+
};
|
|
536
592
|
}
|
|
537
593
|
catch { /* try package layout */ }
|
|
538
594
|
}
|
|
@@ -701,7 +757,7 @@ export function createPanelServer(root, options = {}) {
|
|
|
701
757
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
702
758
|
// Read-only surface: any write method is rejected before routing.
|
|
703
759
|
const runAction = /^\/api\/runs\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\/(pause|resume)$/.test(url.pathname);
|
|
704
|
-
if (request.method !== "GET" && request.method !== "HEAD" && !(request.method === "POST" && (["/api/provider-config", "/api/
|
|
760
|
+
if (request.method !== "GET" && request.method !== "HEAD" && !(request.method === "POST" && (["/api/provider-config", "/api/assistant", "/api/system-prompt", "/api/hooks", "/api/recommendation-state", "/api/install/execute", "/api/active-provider", "/api/artifact/save", "/api/manuscript", "/api/claim", "/api/evidence", "/api/doi/verify", "/api/documents/import", "/api/publish", "/api/references/verify", "/api/references/check", "/api/references/download", "/api/citations"].includes(url.pathname) || runAction))) {
|
|
705
761
|
response.writeHead(405, { "content-type": "application/json", allow: "GET, HEAD" });
|
|
706
762
|
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
707
763
|
return;
|
|
@@ -781,6 +837,22 @@ export function createPanelServer(root, options = {}) {
|
|
|
781
837
|
response.end(JSON.stringify({ schemaVersion: "psyclaw/panel-files/v1", files: await listProjectFiles(root) }));
|
|
782
838
|
return;
|
|
783
839
|
}
|
|
840
|
+
if (url.pathname === "/api/project-files") {
|
|
841
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
842
|
+
response.end(JSON.stringify({ schemaVersion: "psyclaw/panel-project-files/v1", files: await panelProjectFiles(root) }));
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (url.pathname === "/api/project-file") {
|
|
846
|
+
const relative = url.searchParams.get("path") ?? "";
|
|
847
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
848
|
+
response.end(JSON.stringify({ schemaVersion: "psyclaw/panel-project-file/v1", ...(await readPanelProjectFile(root, relative)) }));
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (url.pathname === "/api/traces") {
|
|
852
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
853
|
+
response.end(JSON.stringify(await projectTraceSnapshot({ root })));
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
784
856
|
if (url.pathname === "/api/config") {
|
|
785
857
|
const promptPath = await assertSafeProjectPath(root, ".psyclaw/system-prompt.md");
|
|
786
858
|
let supplement = "";
|
|
@@ -817,6 +889,8 @@ export function createPanelServer(root, options = {}) {
|
|
|
817
889
|
const state = await readRecommendationState(root);
|
|
818
890
|
const disabled = await readDisabledCapabilities(root);
|
|
819
891
|
const [skills, mcps] = await Promise.all([recommendedSkills(), recommendedMcps()]);
|
|
892
|
+
const skillIds = new Set((skills.items ?? []).map((item) => String(item.id ?? "")));
|
|
893
|
+
const mcpIds = new Set((mcps.items ?? []).map((item) => String(item.id ?? "")));
|
|
820
894
|
const nameOf = (kind, id) => {
|
|
821
895
|
const catalog = kind === "skill" ? skills : mcps;
|
|
822
896
|
const item = (catalog.items ?? []).find((candidate) => candidate.id === id);
|
|
@@ -826,8 +900,8 @@ export function createPanelServer(root, options = {}) {
|
|
|
826
900
|
response.end(JSON.stringify({
|
|
827
901
|
schemaVersion: "psyclaw/enabled-capabilities/v1",
|
|
828
902
|
coreSkills: CORE_SKILLS.map((skill) => ({ id: skill.id, name: skill.name, enabled: !disabled.coreSkills.includes(skill.id) })),
|
|
829
|
-
skills: state.skills.map((id) => ({ id, name: nameOf("skill", id), enabled: true })),
|
|
830
|
-
mcp: state.mcp.map((id) => ({ id, name: nameOf("mcp", id), enabled: true })),
|
|
903
|
+
skills: state.skills.filter((id) => skillIds.has(id)).map((id) => ({ id, name: nameOf("skill", id), enabled: true })),
|
|
904
|
+
mcp: state.mcp.filter((id) => mcpIds.has(id)).map((id) => ({ id, name: nameOf("mcp", id), enabled: true })),
|
|
831
905
|
}));
|
|
832
906
|
return;
|
|
833
907
|
}
|
|
@@ -864,6 +938,7 @@ export function createPanelServer(root, options = {}) {
|
|
|
864
938
|
const kind = body.kind === "skill" || body.kind === "mcp" ? body.kind : undefined;
|
|
865
939
|
const id = String(body.id ?? "").trim();
|
|
866
940
|
const approved = body.approved === true;
|
|
941
|
+
const scope = body.scope === "project" || body.scope === "user" ? body.scope : undefined;
|
|
867
942
|
const actor = String(body.actor ?? "researcher").trim();
|
|
868
943
|
if (!kind || !id || !approved || actor.length < 1)
|
|
869
944
|
throw new Error("kind, id, approved and actor are required");
|
|
@@ -873,12 +948,37 @@ export function createPanelServer(root, options = {}) {
|
|
|
873
948
|
response.end(JSON.stringify({ error: "unknown recommended item" }));
|
|
874
949
|
return;
|
|
875
950
|
}
|
|
876
|
-
|
|
877
|
-
|
|
951
|
+
if (kind === "skill") {
|
|
952
|
+
if (!scope)
|
|
953
|
+
throw new Error("scope must be project or user for Skill installation");
|
|
954
|
+
if (options.installSkill === undefined) {
|
|
955
|
+
response.writeHead(503, { "content-type": "application/json" });
|
|
956
|
+
response.end(JSON.stringify({ error: "Skill 安装需要当前模型通道;请在 PsyClaw 对话中执行 /panel 后再安装。", reasonCode: "panel.skill-installer-unavailable" }));
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
await options.installSkill(panelSkillInstallTask(root, found.item, scope));
|
|
960
|
+
const state = await readRecommendationState(root);
|
|
961
|
+
state.skillScopes = { ...(state.skillScopes ?? {}), [id]: scope };
|
|
962
|
+
await writeRecommendationState(root, state);
|
|
963
|
+
response.writeHead(202, { "content-type": "application/json" });
|
|
964
|
+
response.end(JSON.stringify({
|
|
965
|
+
schemaVersion: "psyclaw/model-install-task/v1",
|
|
966
|
+
ok: true,
|
|
967
|
+
queued: true,
|
|
968
|
+
id,
|
|
969
|
+
scope,
|
|
970
|
+
reloadHint: "/reload",
|
|
971
|
+
message: "安装任务已交给当前模型;模型完成后请启用 Skill 并执行 /reload。",
|
|
972
|
+
}));
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const shellCommand = typeof found.prep?.command === "string" && found.prep.command.trim() ? found.prep.command.trim() : undefined;
|
|
976
|
+
if (!shellCommand) {
|
|
878
977
|
response.writeHead(400, { "content-type": "application/json" });
|
|
879
978
|
response.end(JSON.stringify({ error: "no install command is available for this item", blockedReason: found.prep?.blockedReason ?? null }));
|
|
880
979
|
return;
|
|
881
980
|
}
|
|
981
|
+
const command = shellCommand;
|
|
882
982
|
const startedAt = new Date().toISOString();
|
|
883
983
|
const runId = `panel_${randomUUID().replaceAll("-", "")}`;
|
|
884
984
|
const idempotencyKey = `panel:install:${sha256Text(`${kind}\u0000${id}\u0000${actor}`).slice(0, 24)}`;
|
|
@@ -894,7 +994,9 @@ export function createPanelServer(root, options = {}) {
|
|
|
894
994
|
command,
|
|
895
995
|
startedAt,
|
|
896
996
|
};
|
|
897
|
-
|
|
997
|
+
let exitCode;
|
|
998
|
+
let output;
|
|
999
|
+
({ exitCode, output } = await runShellCommand(command, root));
|
|
898
1000
|
receipt.ok = exitCode === 0;
|
|
899
1001
|
receipt.exitCode = exitCode;
|
|
900
1002
|
receipt.finishedAt = new Date().toISOString();
|
|
@@ -902,8 +1004,7 @@ export function createPanelServer(root, options = {}) {
|
|
|
902
1004
|
await atomicWriteFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
903
1005
|
if (receipt.ok) {
|
|
904
1006
|
const state = await readRecommendationState(root);
|
|
905
|
-
|
|
906
|
-
state[key] = [...new Set([...state[key], id])];
|
|
1007
|
+
state.mcp = [...new Set([...state.mcp, id])];
|
|
907
1008
|
await writeRecommendationState(root, state);
|
|
908
1009
|
}
|
|
909
1010
|
await appendJsonlIfMissing(projectPaths(root).audit, {
|
|
@@ -1076,11 +1177,6 @@ export function createPanelServer(root, options = {}) {
|
|
|
1076
1177
|
}
|
|
1077
1178
|
return;
|
|
1078
1179
|
}
|
|
1079
|
-
if (url.pathname === "/api/hitl") {
|
|
1080
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1081
|
-
response.end(JSON.stringify(await readHitlWorkspace(root, url.searchParams.get("contents") === "1")));
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
1180
|
if (url.pathname === "/api/artifacts") {
|
|
1085
1181
|
const indexPath = projectPaths(root).outputs + "/index.json";
|
|
1086
1182
|
let indexedArtifacts = [];
|
|
@@ -1535,63 +1631,6 @@ export function createPanelServer(root, options = {}) {
|
|
|
1535
1631
|
response.end(JSON.stringify({ schemaVersion: "psyclaw/provider-config-receipt/v1", ok: true, provider: id, modelCount: models.length, apiKeyStored: Boolean(apiKey?.trim()) }));
|
|
1536
1632
|
return;
|
|
1537
1633
|
}
|
|
1538
|
-
if (url.pathname === "/api/hitl/decision") {
|
|
1539
|
-
if (request.method !== "POST") {
|
|
1540
|
-
response.writeHead(405, { "content-type": "application/json", allow: "POST" });
|
|
1541
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
1542
|
-
return;
|
|
1543
|
-
}
|
|
1544
|
-
const body = await readJsonBody(request);
|
|
1545
|
-
const decision = String(body.decision ?? "").trim();
|
|
1546
|
-
const rationale = String(body.rationale ?? "").trim();
|
|
1547
|
-
const actor = String(body.actor ?? "researcher").trim();
|
|
1548
|
-
if (!["approved", "denied", "needs-changes"].includes(decision) || rationale.length < 3 || actor.length < 1) {
|
|
1549
|
-
response.writeHead(400, { "content-type": "application/json" });
|
|
1550
|
-
response.end(JSON.stringify({ error: "decision, rationale (3+ chars), and actor are required" }));
|
|
1551
|
-
return;
|
|
1552
|
-
}
|
|
1553
|
-
const recordedAt = new Date().toISOString();
|
|
1554
|
-
const idempotencyKey = `panel:hitl:${sha256Text(`${decision}\u0000${rationale}\u0000${actor}`).slice(0, 24)}`;
|
|
1555
|
-
const decisionDocument = [
|
|
1556
|
-
"---",
|
|
1557
|
-
"schemaVersion: psyclaw/hitl-decision-request/v1",
|
|
1558
|
-
"documentVersion: 1.0.0",
|
|
1559
|
-
`recordedAt: ${recordedAt}`,
|
|
1560
|
-
`actor: ${actor.replaceAll("\n", " ")}`,
|
|
1561
|
-
`decision: ${decision}`,
|
|
1562
|
-
`idempotencyKey: ${idempotencyKey}`,
|
|
1563
|
-
"---",
|
|
1564
|
-
"",
|
|
1565
|
-
"# Decision Request",
|
|
1566
|
-
"",
|
|
1567
|
-
`Decision: ${decision}`,
|
|
1568
|
-
"",
|
|
1569
|
-
"## Rationale",
|
|
1570
|
-
"",
|
|
1571
|
-
rationale,
|
|
1572
|
-
"",
|
|
1573
|
-
].join("\n");
|
|
1574
|
-
const decisionPath = await assertSafeProjectPath(root, "notes/decision_request.md");
|
|
1575
|
-
await atomicWriteFile(decisionPath, decisionDocument);
|
|
1576
|
-
const receipt = {
|
|
1577
|
-
schemaVersion: "psyclaw/tool-receipt/v1",
|
|
1578
|
-
runId: `panel_${randomUUID().replaceAll("-", "")}`,
|
|
1579
|
-
taskId: "hitl-decision",
|
|
1580
|
-
tool: "panel.hitl.decision",
|
|
1581
|
-
effect: "write",
|
|
1582
|
-
approval: "approved",
|
|
1583
|
-
idempotencyKey,
|
|
1584
|
-
ok: true,
|
|
1585
|
-
resultHash: sha256Text(decisionDocument),
|
|
1586
|
-
startedAt: recordedAt,
|
|
1587
|
-
finishedAt: new Date().toISOString(),
|
|
1588
|
-
};
|
|
1589
|
-
const receiptPath = await assertSafeProjectPath(root, `.psyclaw/manifests/${receipt.runId}.receipt.json`);
|
|
1590
|
-
await atomicWriteFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
1591
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1592
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/hitl-decision-receipt/v1", ok: true, decision, decisionPath, receiptPath, idempotencyKey }));
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
1634
|
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
1596
1635
|
const html = await readFile(panelHtmlPath, "utf8");
|
|
1597
1636
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|