@evandu/super-pi 0.1.161 → 0.1.163

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 (34) hide show
  1. package/agents/super-pi-task/core.ts +1 -1
  2. package/agents/super-pi-task/index.ts +1 -1
  3. package/dist/client/.super-pi-client-sources.json +5 -5
  4. package/dist/client/assets/{CodeMirrorEditor-CDaL0eZg.js → CodeMirrorEditor-DEkvKQts.js} +2 -2
  5. package/dist/client/assets/{dist-DO3oDtGc.js → dist-B-8UFI-6.js} +1 -1
  6. package/dist/client/assets/{dist-Cm3rvfSQ.js → dist-BtNbcR8w.js} +1 -1
  7. package/dist/client/assets/{dist-DPwOtaMP.js → dist-C2Uv2gLb.js} +1 -1
  8. package/dist/client/assets/{dist-D1cVuzfF.js → dist-C2mmiivD.js} +1 -1
  9. package/dist/client/assets/{dist-CTelFWCj.js → dist-CNrwgb43.js} +1 -1
  10. package/dist/client/assets/{dist-CiyMaqx4.js → dist-CSiW1PcF.js} +1 -1
  11. package/dist/client/assets/{dist-CZlI9li3.js → dist-CVljZ_cT.js} +1 -1
  12. package/dist/client/assets/{dist-DVCVhi5y.js → dist-CbFClRtF.js} +1 -1
  13. package/dist/client/assets/{dist-BJG0l-sn.js → dist-ChSDUOFD.js} +1 -1
  14. package/dist/client/assets/{dist-gCJkYldU.js → dist-CipOPceB.js} +1 -1
  15. package/dist/client/assets/{dist-DL66Z_jn.js → dist-D5doZ1C8.js} +1 -1
  16. package/dist/client/assets/{dist-DOdfw0L-.js → dist-D5nqZFe3.js} +1 -1
  17. package/dist/client/assets/{dist-DnaLB6eU.js → dist-DC-1StQ4.js} +1 -1
  18. package/dist/client/assets/{dist-BDNs_xhA.js → dist-DYahm0uS.js} +1 -1
  19. package/dist/client/assets/{dist-BZgXABCU.js → dist-Dgkmmss0.js} +1 -1
  20. package/dist/client/assets/{dist-BHWAql8E.js → dist-DpVnCvxg.js} +1 -1
  21. package/dist/client/assets/{dist-DhPkHpbv.js → dist-DwUj9On7.js} +1 -1
  22. package/dist/client/assets/{dist-CSdmsB0d.js → dist-DywKNYXP.js} +1 -1
  23. package/dist/client/assets/{dist-BA58NP6M.js → dist-FPDQhYJa.js} +1 -1
  24. package/dist/client/assets/{dist-S5Gkqadu.js → dist-GFhYndkm.js} +1 -1
  25. package/dist/client/assets/{dist-CWgIVqDh.js → dist-OOixZci6.js} +1 -1
  26. package/dist/client/assets/{dist-jwSzBAO8.js → dist-a40vvieq.js} +1 -1
  27. package/dist/client/assets/{index-B9tN8KD3.js → index-CLV7t20O.js} +10 -10
  28. package/dist/client/assets/{index-BK3ZIqJj.css → index-Wtlj0s4R.css} +1 -1
  29. package/dist/client/index.html +2 -2
  30. package/dist/server/application/project-service.d.ts +19 -1
  31. package/dist/server/application/project-service.js +231 -1
  32. package/dist/server/http-server.js +9 -0
  33. package/dist/server/types.d.ts +33 -0
  34. package/package.json +3 -2
@@ -8,8 +8,8 @@
8
8
  <link rel="alternate icon" href="/favicon.ico" />
9
9
  <link rel="apple-touch-icon" href="/favicon.svg" />
10
10
  <title>Super Pi</title>
11
- <script type="module" crossorigin src="/assets/index-B9tN8KD3.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-BK3ZIqJj.css">
11
+ <script type="module" crossorigin src="/assets/index-CLV7t20O.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-Wtlj0s4R.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
@@ -1,5 +1,5 @@
1
1
  import type { Store } from "../store.js";
2
- import type { Project, ProjectEscalation, ProjectMode, ProjectReport, ProjectRisk, ProjectTask, SessionProfile } from "../types.js";
2
+ import type { Project, ProjectEscalation, ProjectMode, ProjectReport, ProjectRisk, ProjectSummary, ProjectTask, SessionProfile } from "../types.js";
3
3
  import type { SessionOrchestratorService } from "./session-orchestrator-service.js";
4
4
  export declare const profile: (value: unknown, label: string) => SessionProfile | undefined;
5
5
  export declare const mergeProfile: (taskProfile?: SessionProfile, projectDefault?: SessionProfile) => SessionProfile | undefined;
@@ -95,7 +95,25 @@ export declare class ProjectService {
95
95
  private reconcile;
96
96
  private resources;
97
97
  list(workspaceId?: string): Project[];
98
+ headline(p: Project): string;
99
+ private isActorRelated;
100
+ private projectSummary;
101
+ listForActor(workspaceId: string, actor: string): ProjectSummary[];
98
102
  get(id: string, workspaceId?: string): Project;
103
+ /**
104
+ * THE PROJECT AS A REPORT A HUMAN CAN READ.
105
+ *
106
+ * Everything the panel shows in pieces - the objective, the tasks and who did them, the
107
+ * deliveries and their evidence, the reviews that accepted them, the exceptions and how they
108
+ * were decided - in one piece of Markdown, in reading order.
109
+ *
110
+ * TWO THINGS THIS REPORT REFUSES TO DO. It prints no session handle: a conversation id is a
111
+ * machine address, and a report full of addresses is not something a person can read or paste
112
+ * into a weekly note - the session's own title is used instead. And it prints no credential of
113
+ * any kind: the CONTENT of the tasks, events, reports and exceptions is the report, and a
114
+ * capability or a token that happens to live in a session profile is not content.
115
+ */
116
+ exportReport(id: string, workspaceId?: string): Promise<string>;
99
117
  projectForSession(conversationId: string): Project | undefined;
100
118
  create(input: CreateProjectInput, initiatorConversationId?: string): Promise<Project>;
101
119
  private admission;
@@ -122,6 +122,35 @@ const CACHE_WRITE_WEIGHT = 1.25;
122
122
  const tokens = (usage) => usage ? Math.round(usage.input + usage.output + usage.cacheWrite * CACHE_WRITE_WEIGHT + usage.cacheRead * CACHE_READ_WEIGHT) : 0;
123
123
  /** The budget a project starts with, and the floor the one-click raise falls back to. */
124
124
  const DEFAULT_MAX_TOKENS = 100_000;
125
+ // THE WORDS THE REPORT SPEAKS IN. A reader of the report is a person, not a console, so every
126
+ // state and every role is written the way a person would say it out loud.
127
+ const PROJECT_STATUS_TEXT = {
128
+ planning: "草稿", active: "进行中", blocked: "已阻塞", awaiting_approval: "等待审批",
129
+ awaiting_review: "等待复核", completed: "已完成", cancelled: "已取消", failed: "失败",
130
+ };
131
+ const TASK_STATUS_TEXT = {
132
+ pending: "等待依赖", ready: "待派发", in_progress: "执行中", blocked: "已阻塞",
133
+ awaiting_review: "待复核", completed: "已完成", failed: "失败", cancelled: "已取消",
134
+ };
135
+ const EVENT_TEXT = {
136
+ created: "创建项目", started: "启动执行", task_created: "登记任务", dispatched: "派发执行",
137
+ submitted: "提交交付", reviewed: "复核", review_requested: "启动复核", task_updated: "任务更新", status: "状态变更",
138
+ budget: "预算变更", report: "进展汇报", escalated: "例外升级", decision: "决定",
139
+ completed: "项目完成", ready: "依赖就绪",
140
+ };
141
+ const ESCALATION_STATUS_TEXT = { open: "未决", resolved: "已解决", dismissed: "已忽略" };
142
+ const SEVERITY_TEXT = { low: "低", medium: "中", high: "高" };
143
+ const REPORT_KIND_TEXT = {
144
+ progress: "进展", blocked: "阻塞", delivery: "交付", decision_request: "决策请求", review: "复核",
145
+ };
146
+ /** ONE LINE, ONE MEANING: a value that carries newlines must not break the layout of the report. */
147
+ const oneLine = (value) => value.replace(/\r?\n+/g, " ").trim();
148
+ /** A table cell must not carry a pipe of its own, or the row it sits in falls apart. */
149
+ const cell = (value) => oneLine(value).replace(/\|/g, "\\|");
150
+ /** A long delivery is trimmed, and the trim says so - a silent cut would read as the whole truth. */
151
+ const truncate = (value, limit) => value.length > limit ? `${value.slice(0, limit)}…(已截断)` : value;
152
+ /** A timestamp a person can read at a glance, and a missing one that says so. */
153
+ const moment = (value) => value ? value.replace("T", " ").replace(/\.\d+Z$/, "") : "无";
125
154
  /**
126
155
  * THE TWO SHAPES A PROJECT CAN HAVE.
127
156
  *
@@ -302,6 +331,16 @@ export class ProjectService {
302
331
  }
303
332
  }
304
333
  const coordinatorIds = new Set([p.ownerConversationId, ...p.tasks.filter((t) => t.role === "leader").map((t) => t.ownerConversationId)].filter(Boolean));
334
+ /* THE MEMORY TREE OF THE PROJECT, AND OF EVERY SESSION IN IT.
335
+ *
336
+ * Since 0.1.161 a project opens its OWN memory tree: the leader session is the root of it and
337
+ * the workers inherit it, so the project's memories never mix with the Boss's chat. That is
338
+ * invisible from the outside - the client draws a tree of sessions and cannot tell which
339
+ * memories are behind it. So the server says it outright: the tree each session belongs to,
340
+ * and whether that tree is the project's own ("project") or the one it shares with the
341
+ * initiator's conversation ("shared"). The leader is the root of the project's tree, so it is
342
+ * "project" by the same rule as everybody else - there is no special case for it. */
343
+ const projectTreeId = p.ownerConversationId ? this.store.conversation(p.ownerConversationId)?.treeId : undefined;
305
344
  const sessions = state.conversations.filter((c) => c.workspaceId === p.workspaceId && members.has(c.id)).map((c) => ({
306
345
  id: c.id, title: c.title, status: c.status, tokens: tokens(c.usage), contextPercent: c.contextUsage?.percent,
307
346
  // THE TREE SKELETON. The client draws the agents of a project as a tree, and the only
@@ -309,6 +348,10 @@ export class ProjectService {
309
348
  // verbatim (including the undefined case, which means "this session has no parent in
310
349
  // this collection" - i.e. it is a root of the tree).
311
350
  parentConversationId: c.parentConversationId,
351
+ // WHOSE MEMORY THIS SESSION WORKS FROM. The project's own tree is the one the leader
352
+ // opened; anything else is a session that still shares the initiator's chat memory.
353
+ memoryTreeId: c.treeId,
354
+ memoryScope: projectTreeId && c.treeId === projectTreeId ? "project" : "shared",
312
355
  role: c.id === p.ownerConversationId ? "项目负责人" : coordinatorIds.has(c.id) ? "分支 Leader" : p.tasks.some((t) => t.reviewerConversationId === c.id) ? "独立复核" : "执行者",
313
356
  }));
314
357
  const runs = state.subagentRuns.filter((r) => members.has(r.conversationId));
@@ -319,15 +362,198 @@ export class ProjectService {
319
362
  const leaderIsWorker = lightweight(p) && p.ownerConversationId !== undefined;
320
363
  return { tokens: sessions.reduce((sum, s) => sum + s.tokens, 0) + runs.reduce((sum, r) => sum + tokens(r.usage), 0),
321
364
  usageKnown: sessions.length > 0 && sessions.every((s) => Boolean(this.store.conversation(s.id)?.usage)) && runs.every((r) => Boolean(r.usage)),
322
- activeWorkers: sessions.filter((s) => (!coordinatorIds.has(s.id) || (leaderIsWorker && s.id === p.ownerConversationId)) && busy(s.status)).length + runs.filter((r) => r.status === "running").length, sessions };
365
+ activeWorkers: sessions.filter((s) => (!coordinatorIds.has(s.id) || (leaderIsWorker && s.id === p.ownerConversationId)) && busy(s.status)).length + runs.filter((r) => r.status === "running").length,
366
+ // THE PROJECT'S OWN MEMORY TREE, named once so the client does not have to infer it from
367
+ // the sessions. Absent only when the project has no leader session yet.
368
+ memoryTreeId: projectTreeId, sessions };
323
369
  }
324
370
  list(workspaceId) {
325
371
  const scope = this.workspace(workspaceId);
326
372
  return this.store.projects().filter((p) => p.workspaceId === scope).map((p) => this.projectView(p));
327
373
  }
374
+ headline(p) {
375
+ const total = p.tasks.length;
376
+ const completed = p.tasks.filter((t) => t.status === "completed").length;
377
+ if (p.status === "completed") {
378
+ return total > 0 ? `项目已完成(${completed}/${total} 任务已验收)` : "项目已完成";
379
+ }
380
+ if (p.status === "cancelled")
381
+ return "项目已取消";
382
+ if (p.status === "failed")
383
+ return "项目失败";
384
+ const openEscalation = (p.escalations ?? []).find((e) => e.status === "open");
385
+ if (openEscalation) {
386
+ return p.status === "awaiting_approval"
387
+ ? `等待审批:${openEscalation.reason}`
388
+ : `项目已阻塞:${openEscalation.reason}`;
389
+ }
390
+ const blockedTask = p.tasks.find((t) => t.status === "blocked");
391
+ if (blockedTask) {
392
+ const reason = blockedTask.blockedReason ? `(${blockedTask.blockedReason})` : "";
393
+ return `项目已阻塞:${blockedTask.title}${reason}`;
394
+ }
395
+ const inProgressTask = p.tasks.find((t) => t.status === "in_progress");
396
+ if (inProgressTask) {
397
+ return `${completed}/${total} 任务完成,正在执行${inProgressTask.title}`;
398
+ }
399
+ const reviewTask = p.tasks.find((t) => t.status === "awaiting_review");
400
+ if (reviewTask) {
401
+ return `${completed}/${total} 任务完成,正在复核${reviewTask.title}`;
402
+ }
403
+ const readyTask = p.tasks.find((t) => t.status === "ready");
404
+ if (readyTask) {
405
+ return `${completed}/${total} 任务完成,待派发${readyTask.title}`;
406
+ }
407
+ if (total > 0) {
408
+ return `${completed}/${total} 任务完成,等待前置依赖`;
409
+ }
410
+ return "项目规划中,尚未拆解任务";
411
+ }
412
+ isActorRelated(p, actor) {
413
+ if (!actor)
414
+ return false;
415
+ if (actor === p.initiatorConversationId)
416
+ return true;
417
+ if (actor === p.ownerConversationId)
418
+ return true;
419
+ if (p.tasks?.some((t) => t.ownerConversationId === actor || t.reviewerConversationId === actor))
420
+ return true;
421
+ if (this.projectForSession(actor)?.id === p.id)
422
+ return true;
423
+ return false;
424
+ }
425
+ projectSummary(p) {
426
+ const resources = this.resources(p);
427
+ return {
428
+ id: p.id,
429
+ name: p.title,
430
+ status: p.status,
431
+ mode: p.mode ?? "standard",
432
+ headline: this.headline(p),
433
+ tasks: {
434
+ total: p.tasks.length,
435
+ completed: p.tasks.filter((t) => t.status === "completed").length,
436
+ inProgress: p.tasks.filter((t) => t.status === "in_progress").length,
437
+ blocked: p.tasks.filter((t) => t.status === "blocked").length,
438
+ },
439
+ activeWorkers: resources.activeWorkers,
440
+ tokens: resources.tokens,
441
+ updatedAt: p.updatedAt,
442
+ };
443
+ }
444
+ listForActor(workspaceId, actor) {
445
+ const scope = this.workspace(workspaceId);
446
+ return this.store.projects()
447
+ .filter((p) => p.workspaceId === scope && this.isActorRelated(p, actor))
448
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
449
+ .map((p) => this.projectSummary(p));
450
+ }
328
451
  get(id, workspaceId) {
329
452
  return this.projectView(this.raw(id, workspaceId));
330
453
  }
454
+ /**
455
+ * THE PROJECT AS A REPORT A HUMAN CAN READ.
456
+ *
457
+ * Everything the panel shows in pieces - the objective, the tasks and who did them, the
458
+ * deliveries and their evidence, the reviews that accepted them, the exceptions and how they
459
+ * were decided - in one piece of Markdown, in reading order.
460
+ *
461
+ * TWO THINGS THIS REPORT REFUSES TO DO. It prints no session handle: a conversation id is a
462
+ * machine address, and a report full of addresses is not something a person can read or paste
463
+ * into a weekly note - the session's own title is used instead. And it prints no credential of
464
+ * any kind: the CONTENT of the tasks, events, reports and exceptions is the report, and a
465
+ * capability or a token that happens to live in a session profile is not content.
466
+ */
467
+ async exportReport(id, workspaceId) {
468
+ const p = workspaceId === undefined ? this.store.project(id) : this.raw(id, workspaceId);
469
+ if (!p)
470
+ throw new Error("项目不存在或不属于当前工作区");
471
+ const store = this.store;
472
+ /** THE TITLE A PERSON CALLS A SESSION BY. The handle stays out of the report. */
473
+ const session = (conversationId) => {
474
+ if (!conversationId)
475
+ return "系统";
476
+ return store.conversation(conversationId)?.title ?? "未知会话";
477
+ };
478
+ /** THE ROLE A TASK PLAYS, in the words the report uses. */
479
+ const role = (t) => t.role === "leader" ? "负责人" : t.parentTaskId ? "执行者" : "负责人直属";
480
+ /** THE ACCEPTANCE OF A TASK, told the truth about what is behind it. */
481
+ const acceptance = (t) => {
482
+ if (t.review?.lightweight === true)
483
+ return "自评交付(轻量项目,无独立复核)";
484
+ if (!t.review)
485
+ return "未验收";
486
+ return oneLine(t.review.summary);
487
+ };
488
+ /** THE TIMES A DELIVERY WAS SENT BACK. The current review only holds the last word, so the
489
+ * earlier rejections are read back out of the timeline - the event summary carries the exact
490
+ * text the reviewer wrote that day. */
491
+ const reworkNotes = (t) => {
492
+ const prefix = `退回返工:${t.title};`;
493
+ return (p.events ?? []).filter((e) => e.kind === "reviewed" && e.taskId === t.id && e.summary.startsWith(prefix))
494
+ .map((e) => e.summary.slice(prefix.length).trim());
495
+ };
496
+ const lines = [];
497
+ lines.push(`# ${oneLine(p.title)}`, "", `> ${oneLine(p.objective)}`);
498
+ if (p.deliverables.length)
499
+ lines.push(`> 交付物:${p.deliverables.map(oneLine).join(";")}`);
500
+ if (p.acceptanceCriteria.length)
501
+ lines.push(`> 验收标准:${p.acceptanceCriteria.map(oneLine).join(";")}`);
502
+ lines.push("", "## 概览", "");
503
+ lines.push(`- 状态:${PROJECT_STATUS_TEXT[p.status] ?? p.status}`);
504
+ lines.push(`- 模式:${lightweight(p) ? "轻量" : "标准"}`);
505
+ lines.push(`- 风险:${SEVERITY_TEXT[p.risk] ?? p.risk}`);
506
+ lines.push(`- 创建时间:${moment(p.createdAt)}`);
507
+ lines.push(`- 最后更新时间:${moment(p.updatedAt)}`);
508
+ if (p.closedAt)
509
+ lines.push(`- 完成时间:${moment(p.closedAt)}`);
510
+ lines.push(`- 预算上限:${p.budget?.maxTokens ?? DEFAULT_MAX_TOKENS} Token`);
511
+ // THE USAGE IS REPORTED IN THE SAME BILLING-EQUIVALENT UNITS THE BUDGET IS SPENT IN, and the
512
+ // formula is printed next to the number: a budget nobody can predict is a budget nobody trusts.
513
+ lines.push(`- 已用等效 Token:${this.resources(p).tokens}(口径:输入 + 输出 + 缓存写入×${CACHE_WRITE_WEIGHT} + 缓存读取×${CACHE_READ_WEIGHT},取整)`);
514
+ lines.push(`- 任务:共 ${p.tasks.length} 个,已完成 ${p.tasks.filter((t) => t.status === "completed").length} 个`);
515
+ lines.push("", "## 交付与验收", "");
516
+ if (!p.tasks.length)
517
+ lines.push("无");
518
+ p.tasks.forEach((t, index) => {
519
+ lines.push(`### ${index + 1}. ${oneLine(t.title)}`, "");
520
+ lines.push(`- 角色:${role(t)}`);
521
+ lines.push(`- 状态:${TASK_STATUS_TEXT[t.status] ?? t.status}`);
522
+ lines.push(`- 负责人:${t.ownerConversationId ? session(t.ownerConversationId) : "未派发"}`);
523
+ lines.push(`- 复核者:${t.reviewerConversationId ? session(t.reviewerConversationId) : "无"}`);
524
+ lines.push(`- 交付内容:${t.output ? truncate(oneLine(t.output), 200) : "无"}`);
525
+ lines.push(`- 证据:${t.evidenceRefs?.length ? t.evidenceRefs.map(oneLine).join("、") : "无"}`);
526
+ lines.push(`- 验收结论:${acceptance(t)}`);
527
+ if (t.review?.evidenceRefs?.length)
528
+ lines.push(`- 复核证据:${t.review.evidenceRefs.map(oneLine).join("、")}`);
529
+ const notes = reworkNotes(t);
530
+ if (notes.length)
531
+ lines.push(`- 退回意见:${notes.join(";")}`);
532
+ lines.push("");
533
+ });
534
+ lines.push("## 例外与决定", "");
535
+ if (!p.escalations.length)
536
+ lines.push("无");
537
+ else {
538
+ lines.push("| 例外 | 严重程度 | 状态 | 决定 | 解决时间 |", "| --- | --- | --- | --- | --- |");
539
+ for (const e of p.escalations) {
540
+ lines.push(`| ${cell(e.reason)} | ${SEVERITY_TEXT[e.severity] ?? e.severity} | ${ESCALATION_STATUS_TEXT[e.status] ?? e.status} | ${cell(e.decision ?? "无")} | ${e.resolvedAt ? moment(e.resolvedAt) : "无"} |`);
541
+ }
542
+ }
543
+ lines.push("", "## 时间线", "");
544
+ const events = [...(p.events ?? [])].sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
545
+ if (!events.length)
546
+ lines.push("无");
547
+ for (const e of events)
548
+ lines.push(`- ${moment(e.createdAt)} · ${EVENT_TEXT[e.kind] ?? e.kind} · ${oneLine(e.summary)} · ${session(e.actorConversationId)}`);
549
+ lines.push("", "## 最近汇报", "");
550
+ if (!p.reports.length)
551
+ lines.push("无");
552
+ for (const r of p.reports) {
553
+ lines.push(`- ${REPORT_KIND_TEXT[r.kind] ?? r.kind}:${oneLine(r.progress)} | 偏差:${r.variance ? oneLine(r.variance) : "无"} | 下一步:${r.nextStep ? oneLine(r.nextStep) : "无"} | 风险:${r.risks?.length ? r.risks.map(oneLine).join("、") : "无"}`);
554
+ }
555
+ return `${lines.join("\n")}\n`;
556
+ }
331
557
  projectForSession(conversationId) {
332
558
  let c = this.store.conversation(conversationId);
333
559
  const seen = new Set();
@@ -1087,11 +1313,15 @@ export class ProjectService {
1087
1313
  await this.registerTopLevelTasks(project.id, plan, workspaceId, actor);
1088
1314
  return this.get(project.id, workspaceId);
1089
1315
  }
1316
+ if (body.action === "list") {
1317
+ return this.listForActor(workspaceId, actor);
1318
+ }
1090
1319
  const id = text(body.projectId, "projectId");
1091
1320
  const p = this.get(id, workspaceId);
1092
1321
  this.member(p, actor);
1093
1322
  switch (body.action) {
1094
1323
  case "get": return p;
1324
+ case "export": return this.exportReport(id, workspaceId);
1095
1325
  case "start": return this.start(id, workspaceId, actor);
1096
1326
  case "add_task": return this.addTask(id, body, workspaceId, actor);
1097
1327
  case "dispatch": return this.dispatchTask(id, text(body.taskId, "taskId"), workspaceId, actor);
@@ -1551,6 +1551,15 @@ export function createHttpServer(options) {
1551
1551
  json(200, { escalations: project.escalations });
1552
1552
  return;
1553
1553
  }
1554
+ // THE REPORT IS MARKDOWN, NOT JSON. It exists so a person can open the URL in a browser
1555
+ // and read the project, or paste it into a weekly note - a JSON body would need a reader
1556
+ // to have one, and the whole point is that the reader does not.
1557
+ if (subresource === "report") {
1558
+ const markdown = await service.exportReport(projectId, requestedWorkspaceId);
1559
+ response.writeHead(200, { "content-type": "text/markdown; charset=utf-8", "cache-control": "no-store" });
1560
+ response.end(markdown);
1561
+ return;
1562
+ }
1554
1563
  json(404, { error: "未知的项目资源" });
1555
1564
  return;
1556
1565
  }
@@ -1033,10 +1033,25 @@ export type Project = {
1033
1033
  * and any orphan left behind by an older layout). Without it the panel can only show a row
1034
1034
  * per session, and the hierarchy - who reported to whom - stays invisible.
1035
1035
  */
1036
+ /**
1037
+ * The project's live sessions, with the tree skeleton and the memory scope attached.
1038
+ *
1039
+ * `parentConversationId` is what lets the client draw a TREE rather than a flat list: a
1040
+ * session whose parent is not in this collection is a ROOT of the tree (the project leader,
1041
+ * and any orphan left behind by an older layout). Without it the panel can only show a row
1042
+ * per session, and the hierarchy - who reported to whom - stays invisible.
1043
+ *
1044
+ * `memoryScope` says whose memory a session works from: "project" for the sessions that live
1045
+ * in the PROJECT'S OWN memory tree (the leader, and every worker that inherited it), "shared"
1046
+ * for a session that still reads the initiator's chat memory. The panel shows it as a small
1047
+ * mark on every agent, because a reader who cannot tell the two apart cannot tell what a
1048
+ * session remembers.
1049
+ */
1036
1050
  resources?: {
1037
1051
  tokens: number;
1038
1052
  usageKnown: boolean;
1039
1053
  activeWorkers: number;
1054
+ memoryTreeId?: string;
1040
1055
  sessions: {
1041
1056
  id: string;
1042
1057
  title: string;
@@ -1045,6 +1060,8 @@ export type Project = {
1045
1060
  tokens: number;
1046
1061
  contextPercent?: number | null;
1047
1062
  parentConversationId?: string;
1063
+ memoryTreeId?: string;
1064
+ memoryScope?: "project" | "shared";
1048
1065
  }[];
1049
1066
  };
1050
1067
  /**
@@ -1063,6 +1080,22 @@ export type Project = {
1063
1080
  updatedAt: string;
1064
1081
  closedAt?: string;
1065
1082
  };
1083
+ export type ProjectSummary = {
1084
+ id: string;
1085
+ name: string;
1086
+ status: string;
1087
+ mode: "standard" | "lightweight";
1088
+ headline: string;
1089
+ tasks: {
1090
+ total: number;
1091
+ completed: number;
1092
+ inProgress: number;
1093
+ blocked: number;
1094
+ };
1095
+ activeWorkers: number;
1096
+ tokens: number;
1097
+ updatedAt: string;
1098
+ };
1066
1099
  export type AppState = {
1067
1100
  modelFilterRules?: ModelFilterRule[];
1068
1101
  defaultModel?: ModelSelection;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.161",
3
+ "version": "0.1.163",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -223,7 +223,8 @@
223
223
  "scripts/session-report-watchdog.test.mjs",
224
224
  "scripts/session-report-tool.test.mjs",
225
225
  "scripts/session-stop-subtree.test.mjs",
226
- "scripts/awaiting-background-status.test.mjs"
226
+ "scripts/awaiting-background-status.test.mjs",
227
+ "scripts/project-lifecycle-e2e.test.mjs"
227
228
  ],
228
229
  "check:browser": [
229
230
  "scripts/interrupt-browser.test.mjs",