@evandu/super-pi 0.1.153 → 0.1.154

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.
Files changed (44) hide show
  1. package/agents/super-pi-task/core.ts +19 -0
  2. package/agents/super-pi-task/index.ts +17 -0
  3. package/dist/client/.super-pi-client-sources.json +6 -4
  4. package/dist/client/assets/{CodeMirrorEditor-DmRMgD_e.js → CodeMirrorEditor-B7Xs6jNi.js} +2 -2
  5. package/dist/client/assets/{dist-tpn03yX2.js → dist-BAKEacQv.js} +1 -1
  6. package/dist/client/assets/{dist-BTfxEdeF.js → dist-BLt409fN.js} +1 -1
  7. package/dist/client/assets/{dist-BzOPeLJM.js → dist-BdzEUJSL.js} +1 -1
  8. package/dist/client/assets/{dist-mY7dpaip.js → dist-Bl5sUV3i.js} +1 -1
  9. package/dist/client/assets/{dist-BQvKL2T4.js → dist-C1W6MyDX.js} +1 -1
  10. package/dist/client/assets/{dist-h0HvU8ex.js → dist-C8NpSGxO.js} +1 -1
  11. package/dist/client/assets/{dist-Cl_nos6D.js → dist-CAfwLExI.js} +1 -1
  12. package/dist/client/assets/{dist-BWxApOy6.js → dist-CTHQg4Rx.js} +1 -1
  13. package/dist/client/assets/{dist-QCoWWp1z.js → dist-CV2n3nfk.js} +1 -1
  14. package/dist/client/assets/{dist-1ehoscoU.js → dist-CVChVTw-.js} +1 -1
  15. package/dist/client/assets/{dist-BIMTBXpD.js → dist-CpbI--iL.js} +1 -1
  16. package/dist/client/assets/{dist-BaagZpIV.js → dist-CsTjYHwC.js} +1 -1
  17. package/dist/client/assets/{dist-DM15RCDF.js → dist-D-xVK9kg.js} +1 -1
  18. package/dist/client/assets/{dist-BAy39a02.js → dist-D0WeiUzu.js} +1 -1
  19. package/dist/client/assets/{dist-DE8_FyRQ.js → dist-D1WPlyWb.js} +1 -1
  20. package/dist/client/assets/{dist-CMNmeOX_.js → dist-DCKPOd40.js} +1 -1
  21. package/dist/client/assets/{dist-DpK_uu3H.js → dist-DQ4Nk969.js} +1 -1
  22. package/dist/client/assets/{dist-C5OCcu2D.js → dist-D_1md4dq.js} +1 -1
  23. package/dist/client/assets/{dist-YiNVn-GQ.js → dist-Dq0L1Jhh.js} +1 -1
  24. package/dist/client/assets/{dist-SHt_aVda.js → dist-DwQ2g3lZ.js} +1 -1
  25. package/dist/client/assets/{dist-BPU0RZX2.js → dist-TyClJ5X5.js} +1 -1
  26. package/dist/client/assets/{dist-BjZSq94T.js → dist-npMrgpJc.js} +1 -1
  27. package/dist/client/assets/{index-Ce896Hh9.js → index-DX_jpcni.js} +10 -10
  28. package/dist/client/assets/{index-DCLaWQin.css → index-srMSVLBy.css} +1 -1
  29. package/dist/client/index.html +2 -2
  30. package/dist/server/agents.d.ts +1 -1
  31. package/dist/server/agents.js +1 -1
  32. package/dist/server/application/project-service.d.ts +70 -0
  33. package/dist/server/application/project-service.js +573 -0
  34. package/dist/server/http-server.js +189 -2
  35. package/dist/server/index.js +3 -0
  36. package/dist/server/persistence/app-state-repository.d.ts +2 -2
  37. package/dist/server/runtime.js +5 -2
  38. package/dist/server/server-context.d.ts +1 -0
  39. package/dist/server/store.d.ts +5 -1
  40. package/dist/server/store.js +29 -3
  41. package/dist/server/task-tool.d.ts +2 -0
  42. package/dist/server/task-tool.js +2 -0
  43. package/dist/server/types.d.ts +103 -0
  44. package/package.json +3 -1
@@ -314,7 +314,7 @@ export function createHttpServer(options) {
314
314
  // the endpoint that can spend it is exactly the one an agent's own turn can reach, and the
315
315
  // management half (`/api/feishu/config`) stays behind the ordinary token AND the local-address
316
316
  // predicate.
317
- if (requestPath !== "/api/conversation/usage" && requestPath !== "/api/memory/tool" && requestPath !== "/api/knowledge/tool" && requestPath !== "/api/documents/tool" && requestPath !== "/api/web/tool" && requestPath !== "/api/feishu/tool" && requestPath !== "/api/todos" && requestPath !== "/api/scheduled-tasks" && requestPath !== "/api/question" && (requestPath !== "/api/tools" || request.method !== "POST") && !requestPath.startsWith("/tools/") && !isAuthorized(request, accessToken)) {
317
+ if (requestPath !== "/api/conversation/usage" && requestPath !== "/api/memory/tool" && requestPath !== "/api/knowledge/tool" && requestPath !== "/api/documents/tool" && requestPath !== "/api/web/tool" && requestPath !== "/api/feishu/tool" && requestPath !== "/api/todos" && requestPath !== "/api/scheduled-tasks" && requestPath !== "/api/projects/tool" && requestPath !== "/api/question" && (requestPath !== "/api/tools" || request.method !== "POST") && !requestPath.startsWith("/tools/") && !isAuthorized(request, accessToken)) {
318
318
  response.writeHead(401, { "www-authenticate": "Bearer" }).end("Authentication required");
319
319
  return;
320
320
  }
@@ -1176,6 +1176,10 @@ export function createHttpServer(options) {
1176
1176
  };
1177
1177
  try {
1178
1178
  const body = JSON.parse(raw);
1179
+ if (body.conversationId && context.projectService?.projectForSession?.(body.conversationId)) {
1180
+ json(403, { error: "项目分工必须使用 project 派发,以遵守资源准入限制" });
1181
+ return;
1182
+ }
1179
1183
  const result = await runService.dispatchTask(body);
1180
1184
  json(result.status, result.body);
1181
1185
  }
@@ -1302,7 +1306,12 @@ export function createHttpServer(options) {
1302
1306
  response.writeHead(status, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify(body));
1303
1307
  };
1304
1308
  try {
1305
- const result = await runService.runWorkflow(JSON.parse(raw));
1309
+ const body = JSON.parse(raw);
1310
+ if (body.conversationId && context.projectService?.projectForSession?.(body.conversationId)) {
1311
+ json(403, { error: "项目分工必须使用 project 派发,以遵守资源准入限制" });
1312
+ return;
1313
+ }
1314
+ const result = await runService.runWorkflow(body);
1306
1315
  json(result.status, result.body);
1307
1316
  }
1308
1317
  catch (error) {
@@ -1457,6 +1466,180 @@ export function createHttpServer(options) {
1457
1466
  }
1458
1467
  return;
1459
1468
  }
1469
+ if (url.pathname === "/api/projects" || url.pathname.startsWith("/api/projects/")) {
1470
+ if (!isLocalOrPrivateAddress(remote)) {
1471
+ json(403, { error: "项目控制面板只允许本机或私网访问" });
1472
+ return;
1473
+ }
1474
+ const service = context.projectService;
1475
+ if (!service) {
1476
+ json(503, { error: "项目服务未就绪" });
1477
+ return;
1478
+ }
1479
+ const parts = url.pathname.slice("/api/projects".length).split("/").filter(Boolean).map((part) => decodeURIComponent(part));
1480
+ const projectId = parts[0];
1481
+ const subresource = parts[1];
1482
+ const asked = request.method === "GET" ? { text: "{}", tooLarge: false } : await readRequestText(request, MAX_FILE_BYTES * 2);
1483
+ if (asked.tooLarge) {
1484
+ json(413, { error: "请求体过大", code: "too_large" });
1485
+ return;
1486
+ }
1487
+ try {
1488
+ const body = JSON.parse(asked.text);
1489
+ const callerConversationId = typeof body.conversationId === "string" ? body.conversationId.trim() : "";
1490
+ if (subresource === undefined && projectId === "tool") {
1491
+ const verdict = runService.verifyPrimaryCapability(callerConversationId, body.capability);
1492
+ if (!verdict.ok) {
1493
+ json(403, { error: verdict.message });
1494
+ return;
1495
+ }
1496
+ const caller = store.conversation(callerConversationId);
1497
+ if (!caller || !body.workspaceId || caller.workspaceId !== body.workspaceId) {
1498
+ json(403, { error: "workspaceId 与调用会话不一致或缺失" });
1499
+ return;
1500
+ }
1501
+ const result = await service.tool(body, caller.workspaceId, callerConversationId);
1502
+ broadcaster.broadcastState();
1503
+ json(200, { result });
1504
+ return;
1505
+ }
1506
+ if (!isAuthorized(request, accessToken)) {
1507
+ json(401, { error: "需要管理访问凭据" });
1508
+ return;
1509
+ }
1510
+ if (callerConversationId) {
1511
+ const verdict = runService.verifyPrimaryCapability(callerConversationId, body.capability);
1512
+ if (!verdict.ok) {
1513
+ json(403, { error: verdict.message });
1514
+ return;
1515
+ }
1516
+ }
1517
+ const callerWorkspaceId = callerConversationId ? store.conversation(callerConversationId)?.workspaceId : undefined;
1518
+ const explicitWorkspaceId = url.searchParams.get("workspaceId")?.trim() || (typeof body.workspaceId === "string" ? body.workspaceId.trim() : undefined);
1519
+ if (callerWorkspaceId && explicitWorkspaceId && callerWorkspaceId !== explicitWorkspaceId) {
1520
+ json(403, { error: "workspaceId 与调用会话不一致" });
1521
+ return;
1522
+ }
1523
+ if (callerConversationId || body.capability || request.headers["x-super-pi-capability"]) {
1524
+ json(403, { error: "Agent 必须使用 /api/projects/tool,管理操作仅由 Boss 发起" });
1525
+ return;
1526
+ }
1527
+ const requestedWorkspaceId = callerWorkspaceId ?? explicitWorkspaceId;
1528
+ if (request.method === "GET") {
1529
+ if (!requestedWorkspaceId) {
1530
+ json(400, { error: "workspaceId 不能为空" });
1531
+ return;
1532
+ }
1533
+ if (!projectId) {
1534
+ json(200, { projects: service.list(requestedWorkspaceId) });
1535
+ return;
1536
+ }
1537
+ const project = service.get(projectId, requestedWorkspaceId);
1538
+ if (!subresource) {
1539
+ json(200, { project });
1540
+ return;
1541
+ }
1542
+ if (subresource === "tasks") {
1543
+ json(200, { tasks: project.tasks });
1544
+ return;
1545
+ }
1546
+ if (subresource === "reports") {
1547
+ json(200, { reports: project.reports });
1548
+ return;
1549
+ }
1550
+ if (subresource === "escalations") {
1551
+ json(200, { escalations: project.escalations });
1552
+ return;
1553
+ }
1554
+ json(404, { error: "未知的项目资源" });
1555
+ return;
1556
+ }
1557
+ if (request.method !== "POST" && request.method !== "PATCH") {
1558
+ json(405, { error: "项目接口只支持 GET、POST、PATCH" });
1559
+ return;
1560
+ }
1561
+ if (!projectId && request.method === "POST") {
1562
+ const project = await service.create({ ...body, workspaceId: requestedWorkspaceId });
1563
+ broadcaster.broadcastState();
1564
+ json(201, { project });
1565
+ return;
1566
+ }
1567
+ if (!projectId) {
1568
+ json(400, { error: "缺少项目 id" });
1569
+ return;
1570
+ }
1571
+ const workspaceId = requestedWorkspaceId;
1572
+ if (!workspaceId) {
1573
+ json(400, { error: "workspaceId 不能为空" });
1574
+ return;
1575
+ }
1576
+ if (request.method === "POST" && subresource === "start") {
1577
+ const project = await service.start(projectId, workspaceId);
1578
+ broadcaster.broadcastState();
1579
+ json(200, { project });
1580
+ return;
1581
+ }
1582
+ if (request.method === "PATCH" && subresource === "budget") {
1583
+ const project = await service.updateBudget(projectId, body, workspaceId);
1584
+ broadcaster.broadcastState();
1585
+ json(200, { project });
1586
+ return;
1587
+ }
1588
+ if (request.method === "POST" && subresource === "tasks" && parts[2] && parts[3] === "dispatch") {
1589
+ const task = await service.dispatchTask(projectId, parts[2], workspaceId);
1590
+ broadcaster.broadcastState();
1591
+ json(200, { task });
1592
+ return;
1593
+ }
1594
+ if (request.method === "POST" && subresource === "tasks" && parts[2] && parts[3] === "review") {
1595
+ const task = await service.requestReview(projectId, parts[2], workspaceId);
1596
+ broadcaster.broadcastState();
1597
+ json(200, { task });
1598
+ return;
1599
+ }
1600
+ if (request.method === "PATCH" && !subresource) {
1601
+ const project = await service.updateStatus(projectId, body.status, workspaceId);
1602
+ broadcaster.broadcastState();
1603
+ json(200, { project });
1604
+ return;
1605
+ }
1606
+ if (request.method === "POST" && subresource === "tasks") {
1607
+ const task = await service.addTask(projectId, body, workspaceId);
1608
+ broadcaster.broadcastState();
1609
+ json(201, { task });
1610
+ return;
1611
+ }
1612
+ if (request.method === "PATCH" && subresource === "tasks" && parts[2]) {
1613
+ const task = await service.updateTask(projectId, parts[2], { ...body, workspaceId });
1614
+ broadcaster.broadcastState();
1615
+ json(200, { task });
1616
+ return;
1617
+ }
1618
+ if (request.method === "POST" && subresource === "reports") {
1619
+ const report = await service.addReport(projectId, body, workspaceId);
1620
+ broadcaster.broadcastState();
1621
+ json(201, { report });
1622
+ return;
1623
+ }
1624
+ if (request.method === "POST" && subresource === "escalations") {
1625
+ const escalation = await service.addEscalation(projectId, body, workspaceId);
1626
+ broadcaster.broadcastState();
1627
+ json(201, { escalation });
1628
+ return;
1629
+ }
1630
+ if (request.method === "PATCH" && subresource === "escalations" && parts[2]) {
1631
+ const escalation = await service.resolveEscalation(projectId, parts[2], body.decision, body.status ?? "resolved", workspaceId);
1632
+ broadcaster.broadcastState();
1633
+ json(200, { escalation });
1634
+ return;
1635
+ }
1636
+ json(404, { error: "未知的项目操作" });
1637
+ }
1638
+ catch (error) {
1639
+ json(400, { error: error instanceof Error ? error.message : "project request failed" });
1640
+ }
1641
+ return;
1642
+ }
1460
1643
  if (url.pathname === "/api/orchestrator") {
1461
1644
  if (request.method !== "POST" || !isLocalOrPrivateAddress(remote)) {
1462
1645
  response.writeHead(403, { "content-type": "text/plain; charset=utf-8" }).end("Orchestrator requires a loopback or private network POST");
@@ -1484,6 +1667,10 @@ export function createHttpServer(options) {
1484
1667
  return;
1485
1668
  }
1486
1669
  const action = (body.action || "").trim();
1670
+ if ((action === "spawn" || action === "dispatch") && context.projectService?.projectForSession?.(callerConversationId)) {
1671
+ json(403, { error: "项目分工必须使用 project 派发,以遵守资源准入限制" });
1672
+ return;
1673
+ }
1487
1674
  if (action === "spawn") {
1488
1675
  // Capability auth above proves who is calling. The target workspace is a separate, explicit
1489
1676
  // choice: a parentConversationId records a tree edge and never selects that target.
@@ -26,6 +26,7 @@ import { ConversationService } from "./application/conversation-service.js";
26
26
  import { ScheduledTaskService } from "./application/scheduled-task-service.js";
27
27
  import { SessionLedger } from "./session-ledger.js";
28
28
  import { SessionOrchestratorService } from "./application/session-orchestrator-service.js";
29
+ import { ProjectService } from "./application/project-service.js";
29
30
  import { WorkspaceService } from "./application/workspace-service.js";
30
31
  import { CommandHandler } from "./command-handler.js";
31
32
  import { TerminalService, terminalDisabledReason } from "./terminal.js";
@@ -291,6 +292,7 @@ runService.setExecService(execService);
291
292
  const scheduledTaskService = new ScheduledTaskService(store, runService, conversationService, broadcaster);
292
293
  const sessionLedger = new SessionLedger();
293
294
  const sessionOrchestratorService = new SessionOrchestratorService(store, conversationService, runService, sessionLedger, broadcaster);
295
+ const projectService = new ProjectService(store, sessionOrchestratorService);
294
296
  void sessionOrchestratorService.restoreWatchdogs().catch((error) => {
295
297
  console.warn("Super Pi: restoring watchdogs failed:", error instanceof Error ? error.message : String(error));
296
298
  });
@@ -315,6 +317,7 @@ const serverContext = {
315
317
  scheduledTaskService,
316
318
  sessionLedger,
317
319
  sessionOrchestratorService,
320
+ projectService,
318
321
  execService,
319
322
  toolRegistry,
320
323
  toolRunner,
@@ -10,7 +10,7 @@ export type StateTransaction = {
10
10
  run: SubagentRun;
11
11
  } | {
12
12
  kind: "collections.replace";
13
- values: Partial<Pick<AppState, "workflows" | "backgroundTasks" | "scheduledTasks">>;
13
+ values: Partial<Pick<AppState, "workflows" | "backgroundTasks" | "scheduledTasks" | "projects">>;
14
14
  }
15
15
  /**
16
16
  * Removes one collection row entirely.
@@ -22,7 +22,7 @@ export type StateTransaction = {
22
22
  */
23
23
  | {
24
24
  kind: "collections.remove";
25
- name: "workflows" | "backgroundTasks" | "scheduledTasks";
25
+ name: "workflows" | "backgroundTasks" | "scheduledTasks" | "projects";
26
26
  } | {
27
27
  kind: "conversation.metadata";
28
28
  conversationId: string;
@@ -24,7 +24,7 @@ import { LengthExceededError } from "./turn-failure.js";
24
24
  // paths recognise a delivery identically. Re-exported because they were part of this module's surface.
25
25
  export { describeDeliveredUserMessage, describePiQueue } from "./pi-session-events.js";
26
26
  import { createPiSession, createSdkCatalog, piSdkAvailable, resolveTranscriptFile, superPiExtensionPaths } from "./pi-session.js";
27
- import { TASK_CONVERSATION_ENV, PARENT_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_ENDPOINT_ENV, TASK_ROSTER_ENV, TASK_TOOL_NAME, TODO_ENDPOINT_ENV, TODO_TOOL_NAME, SCHEDULED_TASK_ENDPOINT_ENV, SCHEDULED_TASK_TOOL_NAME, WORKFLOW_ENDPOINT_ENV, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, ORCHESTRATOR_ENDPOINT_ENV, ORCHESTRATOR_WORKSPACE_ENV, SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, SESSION_REPORT_TOOL_NAME, SESSION_STOP_SUBTREE_TOOL_NAME, rosterPayload, } from "./task-tool.js";
27
+ import { TASK_CONVERSATION_ENV, PARENT_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_ENDPOINT_ENV, TASK_ROSTER_ENV, TASK_TOOL_NAME, TODO_ENDPOINT_ENV, TODO_TOOL_NAME, PROJECT_TOOL_NAME, PROJECT_ENDPOINT_ENV, SCHEDULED_TASK_ENDPOINT_ENV, SCHEDULED_TASK_TOOL_NAME, WORKFLOW_ENDPOINT_ENV, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, ORCHESTRATOR_ENDPOINT_ENV, ORCHESTRATOR_WORKSPACE_ENV, SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, SESSION_REPORT_TOOL_NAME, SESSION_STOP_SUBTREE_TOOL_NAME, rosterPayload, } from "./task-tool.js";
28
28
  import { BLOCKING_UI_METHODS, EXTENSION_UI_METHODS, } from "./types.js";
29
29
  import { loadConversationImages } from "./upload.js";
30
30
  import { describeToolCall, describeToolCallArgs, describeToolCallInProgress, extractPartialResultText, extractToolResultText, readQuestionCall } from "./tool-calls.js";
@@ -711,6 +711,7 @@ export class PiRpcRuntime {
711
711
  [PARENT_CONVERSATION_ENV]: parentConversationId ?? "",
712
712
  [TASK_DEPTH_ENV]: "0",
713
713
  [TODO_ENDPOINT_ENV]: this.options.todoEndpoint ?? "",
714
+ [PROJECT_ENDPOINT_ENV]: this.options.orchestratorEndpoint?.replace(/\/api\/orchestrator$/, "/api/projects/tool") ?? "",
714
715
  [SCHEDULED_TASK_ENDPOINT_ENV]: this.options.scheduledTaskEndpoint ?? "",
715
716
  [WORKFLOW_ENDPOINT_ENV]: this.options.workflowEndpoint ?? "",
716
717
  [ORCHESTRATOR_ENDPOINT_ENV]: this.options.orchestratorEndpoint ?? "",
@@ -1259,6 +1260,8 @@ export class PiRpcRuntime {
1259
1260
  // loaded-but-undeclared tool - broken with no error to explain it.
1260
1261
  if (process.env.SUPER_PI_FEISHU_ENDPOINT)
1261
1262
  names.push(FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME);
1263
+ if (this.options.orchestratorEndpoint)
1264
+ names.push(PROJECT_TOOL_NAME);
1262
1265
  if (this.options.orchestratorEndpoint)
1263
1266
  names.push(SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, SESSION_REPORT_TOOL_NAME, SESSION_STOP_SUBTREE_TOOL_NAME);
1264
1267
  return names;
@@ -1479,7 +1482,7 @@ export class PiRpcRuntime {
1479
1482
  // allowlist and naming a tool Pi has not registered would change the spawn line for every other
1480
1483
  // mode. The prompt asks for a bare JSON array and nothing else; if the model spends a turn on a
1481
1484
  // tool call anyway the reranker times out and falls back to the lexical order.
1482
- const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : knowledgeRerank ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, SCHEDULED_TASK_TOOL_NAME, "question", "tool_register", ...(this.options.orchestratorEndpoint ? [SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, SESSION_REPORT_TOOL_NAME, SESSION_STOP_SUBTREE_TOOL_NAME] : [])] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : []), ...(knowledgeEnabled ? [KNOWLEDGE_TOOL_NAME, KNOWLEDGE_IMPORT_TOOL_NAME] : []), ...(documentsEnabled ? [DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME] : []), ...(webEnabled ? [WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME] : []), ...(feishuEnabled ? [FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME] : [])] });
1485
+ const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : knowledgeRerank ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, PROJECT_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, SCHEDULED_TASK_TOOL_NAME, "question", "tool_register", ...(this.options.orchestratorEndpoint ? [SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, SESSION_REPORT_TOOL_NAME, SESSION_STOP_SUBTREE_TOOL_NAME] : [])] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : []), ...(knowledgeEnabled ? [KNOWLEDGE_TOOL_NAME, KNOWLEDGE_IMPORT_TOOL_NAME] : []), ...(documentsEnabled ? [DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME] : []), ...(webEnabled ? [WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME] : []), ...(feishuEnabled ? [FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME] : [])] });
1483
1486
  // Refused here, with the reason, rather than handed to the spawn: an ENOENT from `spawn` reads as
1484
1487
  // "the app is broken" and hides the one fact that matters, which is where it looked for Pi.
1485
1488
  if (!this.pi.found)
@@ -73,6 +73,7 @@ export interface ServerContext {
73
73
  scheduledTaskService?: import("./application/scheduled-task-service.js").ScheduledTaskService;
74
74
  sessionLedger?: import("./session-ledger.js").SessionLedger;
75
75
  sessionOrchestratorService?: import("./application/session-orchestrator-service.js").SessionOrchestratorService;
76
+ projectService?: import("./application/project-service.js").ProjectService;
76
77
  execService: ExecService;
77
78
  toolRegistry: ToolRegistry;
78
79
  toolRunner: ToolRunner;
@@ -2,7 +2,7 @@ import type { AppStateRepository } from "./persistence/app-state-repository.js";
2
2
  import { defaultJsonStatePath } from "./persistence/json-state-repository.js";
3
3
  import type { ScheduledTask } from "./scheduled-tasks.js";
4
4
  import type { WorkflowRun } from "./workflow.js";
5
- import type { AppState, Conversation, Message, MessageDelivery, MessageImage, ModelSelection, SubagentRun, SubagentRunUpdate, TodoItem, ToolCallQuestion, ToolCallArg, ToolCallBlock, ToolResultSummary, Workspace } from "./types.js";
5
+ import type { AppState, Conversation, Message, MessageDelivery, MessageImage, ModelSelection, Project, SubagentRun, SubagentRunUpdate, TodoItem, ToolCallQuestion, ToolCallArg, ToolCallBlock, ToolResultSummary, Workspace } from "./types.js";
6
6
  import type { TurnUsageReadout } from "./conversation-usage.js";
7
7
  /** Mints the id a forked conversation will have, BEFORE its Pi transcript exists.
8
8
  *
@@ -145,6 +145,10 @@ export declare class Store {
145
145
  * `nextRunAt` and then hands the whole task back to the store, which owns the real one.
146
146
  */
147
147
  scheduledTasks(): ScheduledTask[];
148
+ projects(): Project[];
149
+ project(projectId: string): Project | undefined;
150
+ setProject(project: Project): Promise<Project>;
151
+ removeProject(projectId: string): Promise<void>;
148
152
  /** Small detached rows used when refreshing each conversation's scoped model catalog. */
149
153
  catalogConversationTargets(): Array<Pick<Conversation, "id" | "archivedAt" | "model">>;
150
154
  /** Clone only one workspace's transcripts, never unrelated workspaces or app state. */
@@ -612,7 +612,8 @@ export class Store {
612
612
  // un-normalised shape rather than the one the store is actually running on.
613
613
  const defaulted = ["workspaces", "conversations", "subagentRuns"].filter((key) => !Array.isArray(state[key])).length > 0
614
614
  || !Array.isArray(state.backgroundTasks)
615
- || (state.scheduledTasks !== undefined && !Array.isArray(state.scheduledTasks));
615
+ || (state.scheduledTasks !== undefined && !Array.isArray(state.scheduledTasks))
616
+ || (state.projects !== undefined && !Array.isArray(state.projects));
616
617
  if (!Array.isArray(state.backgroundTasks))
617
618
  state.backgroundTasks = [];
618
619
  // A scheduled-task list that is not a list is not a list of tasks. The key is dropped rather than
@@ -620,6 +621,8 @@ export class Store {
620
621
  // fresh install reads - instead of crashing the panel on every poll.
621
622
  if (state.scheduledTasks !== undefined && !Array.isArray(state.scheduledTasks))
622
623
  delete state.scheduledTasks;
624
+ if (state.projects !== undefined && !Array.isArray(state.projects))
625
+ delete state.projects;
623
626
  // Every sweep runs before the store is constructed, so a single persist below publishes them together.
624
627
  // Reconciling them in separate writes would put a file on disk in which one kind of orphan was fixed
625
628
  // and the other was not - a half-repaired state that still needs a second restart to finish.
@@ -767,6 +770,28 @@ export class Store {
767
770
  scheduledTasks() {
768
771
  return this.state.scheduledTasks ? [...this.state.scheduledTasks] : [];
769
772
  }
773
+ projects() { return structuredClone(this.state.projects ?? []); }
774
+ project(projectId) { const project = this.state.projects?.find((item) => item.id === projectId); return project ? structuredClone(project) : undefined; }
775
+ async setProject(project) {
776
+ if (!this.state.projects)
777
+ this.state.projects = [];
778
+ const stored = structuredClone(project);
779
+ const index = this.state.projects.findIndex((item) => item.id === stored.id);
780
+ if (index < 0)
781
+ this.state.projects.unshift(stored);
782
+ else
783
+ this.state.projects[index] = stored;
784
+ await this.persistCollections("projects");
785
+ return structuredClone(stored);
786
+ }
787
+ async removeProject(projectId) {
788
+ if (!this.state.projects)
789
+ return;
790
+ this.state.projects = this.state.projects.filter((item) => item.id !== projectId);
791
+ if (!this.state.projects.length)
792
+ delete this.state.projects;
793
+ await this.persistCollections("projects");
794
+ }
770
795
  /** Small detached rows used when refreshing each conversation's scoped model catalog. */
771
796
  catalogConversationTargets() {
772
797
  return this.state.conversations.map(({ id, archivedAt, model }) => ({
@@ -1030,13 +1055,14 @@ export class Store {
1030
1055
  * sidebar renders `state.workspaces` and never has to know that a closed workspace exists.
1031
1056
  */
1032
1057
  clientState(scope) {
1058
+ const { projects: _projects, ...publicState } = this.state;
1033
1059
  const closed = new Set(this.state.workspaces.filter((item) => item.closedAt).map((item) => item.id));
1034
1060
  const workspaces = this.state.workspaces.filter((item) => !closed.has(item.id));
1035
1061
  const visible = this.state.conversations.filter((item) => !closed.has(item.workspaceId));
1036
1062
  // Legacy sockets still receive complete visible transcripts. Project before cloning so a closed
1037
1063
  // workspace's retained history is never copied just to discard it afterwards.
1038
1064
  if (scope === undefined)
1039
- return structuredClone({ ...this.state, workspaces, conversations: visible, serverTimeZone: serverTimeZone() });
1065
+ return structuredClone({ ...publicState, workspaces, conversations: visible, serverTimeZone: serverTimeZone() });
1040
1066
  // A focused socket keeps every sidebar row, but owns only the selected transcript. An empty scope
1041
1067
  // deliberately loads none; a missing/closed selection must not expose retained workspace history.
1042
1068
  const selectedId = visible.find((item) => item.id === scope.conversationId)?.id;
@@ -1047,7 +1073,7 @@ export class Store {
1047
1073
  hasPendingQuestion: messages.some((message) => message.toolCalls?.some((call) => call.question?.status === "pending")),
1048
1074
  }));
1049
1075
  return structuredClone({
1050
- ...this.state,
1076
+ ...publicState,
1051
1077
  workspaces,
1052
1078
  conversations,
1053
1079
  // THE CLOCK THE SERVER SCHEDULES BY, NAMED. The scheduled-task panel renders 「每天 09:00」 and
@@ -162,6 +162,8 @@ export declare function parentDeniedTools(parent: AgentDefinition | undefined):
162
162
  * `todowrite` and not `todo`, and there is nothing to gain from renaming a tool whose description has
163
163
  * already been written to explain that name.
164
164
  */
165
+ export declare const PROJECT_TOOL_NAME = "project";
166
+ export declare const PROJECT_ENDPOINT_ENV = "SUPER_PI_PROJECT_ENDPOINT";
165
167
  export declare const TODO_TOOL_NAME = "todowrite";
166
168
  /**
167
169
  * The endpoint the `todowrite` extension dials. A FIFTH name in the same wire as the four above, written
@@ -219,6 +219,8 @@ export function parentDeniedTools(parent) {
219
219
  * `todowrite` and not `todo`, and there is nothing to gain from renaming a tool whose description has
220
220
  * already been written to explain that name.
221
221
  */
222
+ export const PROJECT_TOOL_NAME = "project";
223
+ export const PROJECT_ENDPOINT_ENV = "SUPER_PI_PROJECT_ENDPOINT";
222
224
  export const TODO_TOOL_NAME = "todowrite";
223
225
  /**
224
226
  * The endpoint the `todowrite` extension dials. A FIFTH name in the same wire as the four above, written
@@ -898,6 +898,108 @@ export type TerminalShell = {
898
898
  file: string;
899
899
  args: string[];
900
900
  };
901
+ export type ProjectStatus = "planning" | "active" | "blocked" | "awaiting_approval" | "awaiting_review" | "completed" | "failed" | "cancelled";
902
+ export type ProjectTaskStatus = "pending" | "ready" | "in_progress" | "blocked" | "awaiting_review" | "completed" | "failed" | "cancelled";
903
+ export type ProjectRisk = "low" | "medium" | "high";
904
+ export type ProjectTask = {
905
+ parentTaskId?: string;
906
+ role?: "leader" | "executor";
907
+ id: string;
908
+ projectId: string;
909
+ title: string;
910
+ objective: string;
911
+ status: ProjectTaskStatus;
912
+ ownerConversationId?: string;
913
+ accountableConversationId?: string;
914
+ reviewerConversationId?: string;
915
+ dependencies: string[];
916
+ deliverables: string[];
917
+ acceptanceCriteria: string[];
918
+ priority: "low" | "medium" | "high" | "urgent";
919
+ dueAt?: string;
920
+ blockedReason?: string;
921
+ output?: string;
922
+ evidenceRefs?: string[];
923
+ review?: {
924
+ verdict: "accepted" | "changes_requested";
925
+ summary: string;
926
+ evidenceRefs: string[];
927
+ reviewerConversationId?: string;
928
+ createdAt: string;
929
+ };
930
+ createdAt: string;
931
+ updatedAt: string;
932
+ };
933
+ export type ProjectReport = {
934
+ id: string;
935
+ projectId: string;
936
+ taskId?: string;
937
+ authorConversationId?: string;
938
+ kind: "progress" | "blocked" | "delivery" | "decision_request" | "review";
939
+ progress: string;
940
+ variance?: string;
941
+ nextStep?: string;
942
+ risks?: string[];
943
+ decisionNeeded?: string;
944
+ evidenceRefs?: string[];
945
+ createdAt: string;
946
+ };
947
+ export type ProjectEscalation = {
948
+ id: string;
949
+ projectId: string;
950
+ taskId?: string;
951
+ reason: string;
952
+ severity: ProjectRisk;
953
+ status: "open" | "resolved" | "dismissed";
954
+ requestedByConversationId?: string;
955
+ decision?: string;
956
+ createdAt: string;
957
+ resolvedAt?: string;
958
+ };
959
+ export type ProjectEvent = {
960
+ id: string;
961
+ kind: string;
962
+ summary: string;
963
+ taskId?: string;
964
+ actorConversationId?: string;
965
+ createdAt: string;
966
+ };
967
+ export type Project = {
968
+ id: string;
969
+ workspaceId: string;
970
+ title: string;
971
+ objective: string;
972
+ deliverables: string[];
973
+ acceptanceCriteria: string[];
974
+ constraints: string[];
975
+ risk: ProjectRisk;
976
+ status: ProjectStatus;
977
+ ownerConversationId?: string;
978
+ budget?: {
979
+ maxTokens?: number;
980
+ maxConcurrent?: number;
981
+ };
982
+ events?: ProjectEvent[];
983
+ resources?: {
984
+ tokens: number;
985
+ usageKnown: boolean;
986
+ activeWorkers: number;
987
+ sessions: {
988
+ id: string;
989
+ title: string;
990
+ role: string;
991
+ status: RunStatus;
992
+ tokens: number;
993
+ contextPercent?: number | null;
994
+ }[];
995
+ };
996
+ tasks: ProjectTask[];
997
+ reports: ProjectReport[];
998
+ escalations: ProjectEscalation[];
999
+ createdAt: string;
1000
+ updatedAt: string;
1001
+ closedAt?: string;
1002
+ };
901
1003
  export type AppState = {
902
1004
  modelFilterRules?: ModelFilterRule[];
903
1005
  defaultModel?: ModelSelection;
@@ -909,6 +1011,7 @@ export type AppState = {
909
1011
  todosByConversation?: Record<string, TodoItem[]>;
910
1012
  workflows?: WorkflowRun[];
911
1013
  scheduledTasks?: ScheduledTask[];
1014
+ projects?: Project[];
912
1015
  customAgentDirs?: string[]; /**
913
1016
  * Which conversations the reader has collapsed in the sidebar's session tree, by conversation id.
914
1017
  * Server-side so the tree looks the same after a restart and on another machine - the sidebar is a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.153",
3
+ "version": "0.1.154",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -83,6 +83,8 @@
83
83
  "scripts/session-result-callback.test.mjs",
84
84
  "scripts/session-result-callback-e2e.test.mjs",
85
85
  "scripts/session-orchestrator-tools.test.mjs",
86
+ "scripts/project-service.test.mjs",
87
+ "scripts/project-api-scope.test.mjs",
86
88
  "scripts/session-tree-collapse.test.mjs",
87
89
  "scripts/archive-cascade.test.mjs",
88
90
  "scripts/workspace-cwd.test.mjs",