@evandu/super-pi 0.1.167 → 0.1.168

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 (35) hide show
  1. package/agents/super-pi-task/core.ts +1 -1
  2. package/dist/client/.super-pi-client-sources.json +4 -4
  3. package/dist/client/assets/{CodeMirrorEditor-CVWde5kc.js → CodeMirrorEditor-DWEFdLq1.js} +2 -2
  4. package/dist/client/assets/{dist-GZFbu49l.js → dist-0cjcfZm8.js} +1 -1
  5. package/dist/client/assets/{dist-BmTCWOAf.js → dist-3mX68Ead.js} +1 -1
  6. package/dist/client/assets/{dist-CYucB1mz.js → dist-84wmy0xb.js} +1 -1
  7. package/dist/client/assets/{dist-DOYAWZox.js → dist-B19er0Hr.js} +1 -1
  8. package/dist/client/assets/{dist-DTOebxDL.js → dist-BWN1gFkd.js} +1 -1
  9. package/dist/client/assets/{dist-DaEO5rUQ.js → dist-Bh9F9CwL.js} +1 -1
  10. package/dist/client/assets/{dist-D7VBYFBQ.js → dist-BiVenLEE.js} +1 -1
  11. package/dist/client/assets/{dist-FPYonm2j.js → dist-Bp858rkg.js} +1 -1
  12. package/dist/client/assets/{dist-BNHp4wy-.js → dist-BsaizV0U.js} +1 -1
  13. package/dist/client/assets/{dist-C5vtfmg_.js → dist-BygS3RZC.js} +1 -1
  14. package/dist/client/assets/{dist-C44HyASP.js → dist-CPZVRFqI.js} +1 -1
  15. package/dist/client/assets/{dist-BCl45PhR.js → dist-CXIdy9EI.js} +1 -1
  16. package/dist/client/assets/{dist-RLM_LWKf.js → dist-Co6-VsRy.js} +1 -1
  17. package/dist/client/assets/{dist-DQ97Hq65.js → dist-D5SNpvpB.js} +1 -1
  18. package/dist/client/assets/{dist-DInnP2s9.js → dist-D9UIUBjg.js} +1 -1
  19. package/dist/client/assets/{dist-4fGafgy-.js → dist-DR3gLDod.js} +1 -1
  20. package/dist/client/assets/{dist-BHFNgK-o.js → dist-DTYN3KZG.js} +1 -1
  21. package/dist/client/assets/{dist-CH83_wek.js → dist-ELtcJv97.js} +1 -1
  22. package/dist/client/assets/{dist-BmMZE70t.js → dist-H7kWZ7Cr.js} +1 -1
  23. package/dist/client/assets/{dist-DUgYAI4d.js → dist-XSVtuaKi.js} +1 -1
  24. package/dist/client/assets/{dist-CC90sIpG.js → dist-ei0l0QpD.js} +1 -1
  25. package/dist/client/assets/{dist-Dn-GoU5Q.js → dist-i063EgJc.js} +1 -1
  26. package/dist/client/assets/{index-DWjVnvsQ.css → index-C3ETe3EL.css} +1 -1
  27. package/dist/client/assets/{index-BptmWfVR.js → index-C4d7l2t8.js} +10 -10
  28. package/dist/client/index.html +2 -2
  29. package/dist/server/agents.d.ts +18 -0
  30. package/dist/server/agents.js +29 -0
  31. package/dist/server/application/project-service.d.ts +47 -3
  32. package/dist/server/application/project-service.js +75 -9
  33. package/dist/server/index.js +13 -2
  34. package/dist/server/types.d.ts +6 -2
  35. package/package.json +6 -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-BptmWfVR.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-DWjVnvsQ.css">
11
+ <script type="module" crossorigin src="/assets/index-C4d7l2t8.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-C3ETe3EL.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
@@ -199,6 +199,24 @@ export declare function readAgents(directory: string | readonly string[], option
199
199
  mode?: AgentModeFilter;
200
200
  builtinDir?: string;
201
201
  }): Promise<AgentScan>;
202
+ /**
203
+ * WHAT KIND OF AGENT IS THIS ID?
204
+ *
205
+ * The catalogue is read in halves - the primaries and the subagents - and a question about ONE id
206
+ * has to be asked of both halves. The answer is the `mode:` frontmatter of the definition that
207
+ * WINS, which is the same override rule `readAgents` already applies: the first directory to offer
208
+ * an id owns it.
209
+ *
210
+ * `undefined` MEANS "NO SUCH AGENT IN THESE DIRECTORIES", and that is NOT the same as "not
211
+ * primary". An id nobody has heard of is a CUSTOM agent, and custom agents are legal - §3 keeps
212
+ * them legal on purpose, so that a project can name an agent the catalogue does not carry yet.
213
+ * Only an agent that EXISTS and is a subagent is a subagent.
214
+ *
215
+ * THE HIDDEN ONES COUNT. `hidden` decides whether a person is SHOWN an agent in a picker, not
216
+ * whether the agent exists, so a hidden subagent is still a subagent here. A guard that could be
217
+ * walked around by ticking one box in a frontmatter would not be a guard.
218
+ */
219
+ export declare function agentModeOf(directories: string | readonly string[], agentId: string): Promise<"primary" | "subagent" | undefined>;
202
220
  /** Parses one definition file, or returns the reason it is not a usable agent. */
203
221
  export declare function parseDefinition(file: string, source: string): AgentDefinition | AgentProblem;
204
222
  /**
@@ -545,6 +545,35 @@ export async function readAgents(directory, options = {}) {
545
545
  problems,
546
546
  };
547
547
  }
548
+ /**
549
+ * WHAT KIND OF AGENT IS THIS ID?
550
+ *
551
+ * The catalogue is read in halves - the primaries and the subagents - and a question about ONE id
552
+ * has to be asked of both halves. The answer is the `mode:` frontmatter of the definition that
553
+ * WINS, which is the same override rule `readAgents` already applies: the first directory to offer
554
+ * an id owns it.
555
+ *
556
+ * `undefined` MEANS "NO SUCH AGENT IN THESE DIRECTORIES", and that is NOT the same as "not
557
+ * primary". An id nobody has heard of is a CUSTOM agent, and custom agents are legal - §3 keeps
558
+ * them legal on purpose, so that a project can name an agent the catalogue does not carry yet.
559
+ * Only an agent that EXISTS and is a subagent is a subagent.
560
+ *
561
+ * THE HIDDEN ONES COUNT. `hidden` decides whether a person is SHOWN an agent in a picker, not
562
+ * whether the agent exists, so a hidden subagent is still a subagent here. A guard that could be
563
+ * walked around by ticking one box in a frontmatter would not be a guard.
564
+ */
565
+ export async function agentModeOf(directories, agentId) {
566
+ const id = agentId.trim();
567
+ if (!id)
568
+ return undefined;
569
+ const dirs = Array.isArray(directories) ? [...directories] : [directories];
570
+ for (const mode of ["subagent", "primary"]) {
571
+ const scan = await readAgents(dirs, { mode });
572
+ if (scan.agents.some((agent) => agent.id === id))
573
+ return mode;
574
+ }
575
+ return undefined;
576
+ }
548
577
  /** Only `#rgb` and `#rrggbb`. A `color:` frontmatter value is rendered as a CSS background, so it has to
549
578
  * be a colour and nothing else - accepting an arbitrary string would put author-controlled text into a
550
579
  * style declaration. Anything else is dropped rather than sanitised into something misleading. */
@@ -80,12 +80,56 @@ export type BudgetRaiseResult = {
80
80
  /** How many budget exceptions were closed by the raise. */
81
81
  resolvedEscalations: number;
82
82
  };
83
+ /**
84
+ * HOW THE SERVICE ASKS WHAT AN AGENT ID IS.
85
+ *
86
+ * The employee rule is a fact about the agent DIRECTORY, so the directory is read through the one
87
+ * reader the server already has (`readAgents`, through `agentModeOf`), and the question is asked of
88
+ * whoever owns those directories. The service deliberately does not know where the definitions
89
+ * live: the directories change when the user adds one, and a service that kept its own copy of the
90
+ * path would answer about a directory that is no longer there.
91
+ *
92
+ * `undefined` means "no such agent" - NOT "not primary". An id that is in no directory is a custom
93
+ * agent, and §3 keeps custom agents legal.
94
+ */
95
+ export type AgentModeLookup = {
96
+ agentModeOf(agentId: string): Promise<"primary" | "subagent" | undefined>;
97
+ };
83
98
  /** Persist explicit domain fields only; transport credentials never enter project records. */
84
99
  export declare class ProjectService {
85
100
  private readonly store;
86
101
  private readonly orchestrator?;
102
+ /**
103
+ * Optional on purpose: a service built without an agent directory cannot tell a subagent from a
104
+ * custom agent, and it then behaves exactly as it did before this check existed. The real
105
+ * server always passes one.
106
+ */
107
+ private readonly agents?;
87
108
  private readonly queues;
88
- constructor(store: Store, orchestrator?: (Pick<SessionOrchestratorService, "spawn" | "dispatch"> & Partial<Pick<SessionOrchestratorService, "stopSubtree">>) | undefined);
109
+ constructor(store: Store, orchestrator?: (Pick<SessionOrchestratorService, "spawn" | "dispatch"> & Partial<Pick<SessionOrchestratorService, "stopSubtree">>) | undefined,
110
+ /**
111
+ * Optional on purpose: a service built without an agent directory cannot tell a subagent from a
112
+ * custom agent, and it then behaves exactly as it did before this check existed. The real
113
+ * server always passes one.
114
+ */
115
+ agents?: AgentModeLookup | undefined);
116
+ /**
117
+ * A PROJECT EMPLOYEE MUST BE A PRIMARY AGENT.
118
+ *
119
+ * §1 says a subagent cannot be a project employee: a subagent is a tool-level, one-shot,
120
+ * invisible call, and a project that leans on one leans on a role the user can never see and
121
+ * never interrupt. The client only offers primary agents in the candidate list, but a candidate
122
+ * list is a SUGGESTION - the server is the constraint, and the refusal belongs at the moment the
123
+ * profile is written, while the reader is still looking at the form. Refusing later (when the
124
+ * task is dispatched) teaches the reader that the panel accepted something the project cannot
125
+ * use.
126
+ *
127
+ * WHAT IS REFUSED, AND WHAT IS NOT. An agent that EXISTS and is a subagent is refused, and the
128
+ * message names the slot and the agent, so the reader knows WHICH employee to change. An id that
129
+ * is in NO directory is a custom agent, and §3 keeps custom agents legal - so it is accepted here,
130
+ * exactly as it was before this check existed.
131
+ */
132
+ private assertEmployee;
89
133
  static taskView(task: ProjectTask, project: Pick<Project, "defaultExecutorProfile" | "defaultReviewerProfile">): ProjectTask;
90
134
  private filterEscalations;
91
135
  private projectView;
@@ -265,8 +309,8 @@ export declare class ProjectService {
265
309
  */
266
310
  raiseBudgetAndContinue(id: string, workspaceId: string, callerConversationId?: string): Promise<BudgetRaiseResult>;
267
311
  addReport(id: string, input: Omit<ProjectReport, "id" | "projectId" | "createdAt">, workspaceId?: string, actor?: string): Promise<ProjectReport>;
268
- addEscalation(id: string, input: Pick<ProjectEscalation, "reason" | "severity" | "taskId">, workspaceId?: string, actor?: string): Promise<ProjectEscalation>;
269
- resolveEscalation(id: string, escalationId: string, decision: string, status: "resolved" | "dismissed", workspaceId?: string, actor?: string): Promise<ProjectEscalation>;
312
+ addEscalation(id: string, input: Pick<ProjectEscalation, "reason" | "severity" | "taskId" | "title">, workspaceId?: string, actor?: string): Promise<ProjectEscalation>;
313
+ resolveEscalation(id: string, escalationId: string, decision: string, status: "resolved" | "dismissed" | "returned", workspaceId?: string, actor?: string): Promise<ProjectEscalation>;
270
314
  complete(id: string, summary: string, evidenceRefs: unknown, workspaceId: string, actor?: string): Promise<Project>;
271
315
  /**
272
316
  * The tasks an initiator hands over with create. A dependency names the TITLE of another task in
@@ -171,7 +171,7 @@ const EVENT_TEXT = {
171
171
  budget: "预算变更", report: "进展汇报", escalated: "例外升级", decision: "决定",
172
172
  completed: "项目完成", ready: "依赖就绪", unreachable: "转述对象不可达",
173
173
  };
174
- const ESCALATION_STATUS_TEXT = { open: "未决", resolved: "已解决", dismissed: "已忽略" };
174
+ const ESCALATION_STATUS_TEXT = { open: "未决", resolved: "已解决", dismissed: "已忽略", returned: "已退回补充材料" };
175
175
  const SEVERITY_TEXT = { low: "低", medium: "中", high: "高" };
176
176
  const REPORT_KIND_TEXT = {
177
177
  progress: "进展", blocked: "阻塞", delivery: "交付", decision_request: "决策请求", review: "复核",
@@ -202,10 +202,43 @@ const lightweight = (p) => p.mode === "lightweight";
202
202
  export class ProjectService {
203
203
  store;
204
204
  orchestrator;
205
+ agents;
205
206
  queues = new Map();
206
- constructor(store, orchestrator) {
207
+ constructor(store, orchestrator,
208
+ /**
209
+ * Optional on purpose: a service built without an agent directory cannot tell a subagent from a
210
+ * custom agent, and it then behaves exactly as it did before this check existed. The real
211
+ * server always passes one.
212
+ */
213
+ agents) {
207
214
  this.store = store;
208
215
  this.orchestrator = orchestrator;
216
+ this.agents = agents;
217
+ }
218
+ /**
219
+ * A PROJECT EMPLOYEE MUST BE A PRIMARY AGENT.
220
+ *
221
+ * §1 says a subagent cannot be a project employee: a subagent is a tool-level, one-shot,
222
+ * invisible call, and a project that leans on one leans on a role the user can never see and
223
+ * never interrupt. The client only offers primary agents in the candidate list, but a candidate
224
+ * list is a SUGGESTION - the server is the constraint, and the refusal belongs at the moment the
225
+ * profile is written, while the reader is still looking at the form. Refusing later (when the
226
+ * task is dispatched) teaches the reader that the panel accepted something the project cannot
227
+ * use.
228
+ *
229
+ * WHAT IS REFUSED, AND WHAT IS NOT. An agent that EXISTS and is a subagent is refused, and the
230
+ * message names the slot and the agent, so the reader knows WHICH employee to change. An id that
231
+ * is in NO directory is a custom agent, and §3 keeps custom agents legal - so it is accepted here,
232
+ * exactly as it was before this check existed.
233
+ */
234
+ async assertEmployee(profile, label) {
235
+ const agentId = profile?.agentId;
236
+ if (!agentId || !this.agents)
237
+ return;
238
+ const mode = await this.agents.agentModeOf(agentId);
239
+ if (mode === "subagent") {
240
+ throw new Error(`${label}.agentId 指向的是 subagent(${agentId}),项目员工必须是 primary:subagent 是一次性的工具级调用,用户在侧栏看不到它的中间步骤,也插不上话。`);
241
+ }
209
242
  }
210
243
  static taskView(task, project) {
211
244
  const t = { ...task };
@@ -820,6 +853,12 @@ export class ProjectService {
820
853
  const defaultLeaderProfile = profile(input.defaultLeaderProfile, "默认 Leader 配置");
821
854
  const defaultExecutorProfile = profile(input.defaultExecutorProfile, "默认执行配置");
822
855
  const defaultReviewerProfile = profile(input.defaultReviewerProfile, "默认复核配置");
856
+ // THE PROJECT'S OWN EMPLOYEE DEFAULTS ARE EMPLOYEES TOO. They are the profiles every task
857
+ // inherits when it names no one, so a subagent written here would become the employee of the
858
+ // whole project - and it would be refused at every task that inherited it, one by one, long
859
+ // after the person who wrote it had walked away.
860
+ await this.assertEmployee(defaultExecutorProfile, "defaultExecutorProfile");
861
+ await this.assertEmployee(defaultReviewerProfile, "defaultReviewerProfile");
823
862
  const initiator = (initiatorConversationId ?? (typeof input.initiatorConversationId === "string" ? input.initiatorConversationId : undefined))?.trim() || undefined;
824
863
  const now = stamp();
825
864
  const p = { id: `project_${randomUUID()}`, workspaceId, title: text(input.title, "项目标题"), objective: text(input.objective, "项目目标"),
@@ -909,7 +948,7 @@ export class ProjectService {
909
948
  });
910
949
  }
911
950
  async addTask(id, input, workspaceId, actor) {
912
- return this.change(id, workspaceId, (p) => {
951
+ return this.change(id, workspaceId, async (p) => {
913
952
  this.live(p);
914
953
  const parentTaskId = optional(input.parentTaskId, "父任务 id");
915
954
  // A LIGHTWEIGHT PROJECT HAS ONE SESSION, SO IT HAS NO TREE OF TASKS. Decomposing the work
@@ -961,6 +1000,11 @@ export class ProjectService {
961
1000
  throw new Error("项目最多 100 个任务,请收敛分工范围");
962
1001
  const executorProfile = profile(input.executorProfile, "执行配置");
963
1002
  const reviewerProfile = profile(input.reviewerProfile, "复核配置");
1003
+ // THE EMPLOYEE IS CHECKED BEFORE THE TASK EXISTS. The refusal happens in the write itself,
1004
+ // so a project never carries a task whose employee is a subagent - not even for the moment
1005
+ // between the write and the next read.
1006
+ await this.assertEmployee(executorProfile, "executorProfile");
1007
+ await this.assertEmployee(reviewerProfile, "reviewerProfile");
964
1008
  const now = stamp();
965
1009
  const t = { id: `task_${randomUUID()}`, projectId: id, title: text(input.title, "任务标题"), objective: text(input.objective, "任务目标"),
966
1010
  status: dependencies.length ? "pending" : "ready", accountableConversationId: manager, parentTaskId, role: input.role ?? "executor", dependencies,
@@ -1126,7 +1170,7 @@ export class ProjectService {
1126
1170
  });
1127
1171
  }
1128
1172
  async updateTask(id, taskId, patch, actor) {
1129
- const result = await this.change(id, patch.workspaceId, (p) => {
1173
+ const result = await this.change(id, patch.workspaceId, async (p) => {
1130
1174
  this.live(p);
1131
1175
  const t = this.task(p, taskId);
1132
1176
  this.member(p, actor);
@@ -1160,6 +1204,9 @@ export class ProjectService {
1160
1204
  if (t.ownerConversationId)
1161
1205
  throw new Error("执行会话已创建,禁止修改执行配置");
1162
1206
  const ep = profile(patch.executorProfile, "执行配置");
1207
+ // THE CHECK COMES BEFORE THE ASSIGNMENT. The task keeps the employee it had; what it must
1208
+ // not do is end up holding an employee the project is not allowed to have.
1209
+ await this.assertEmployee(ep, "executorProfile");
1163
1210
  if (ep)
1164
1211
  t.executorProfile = ep;
1165
1212
  else
@@ -1169,6 +1216,7 @@ export class ProjectService {
1169
1216
  if (t.reviewerConversationId)
1170
1217
  throw new Error("复核会话已创建,禁止修改复核配置");
1171
1218
  const rp = profile(patch.reviewerProfile, "复核配置");
1219
+ await this.assertEmployee(rp, "reviewerProfile");
1172
1220
  if (rp)
1173
1221
  t.reviewerProfile = rp;
1174
1222
  else
@@ -1374,7 +1422,7 @@ export class ProjectService {
1374
1422
  if (actor && actor !== p.ownerConversationId && (!t || ![t.ownerConversationId, t.reviewerConversationId, this.manager(p, t)].includes(actor)))
1375
1423
  throw new Error("仅可汇报本人负责的任务");
1376
1424
  const report = { id: randomUUID(), projectId: id, taskId: t?.id, authorConversationId: actor, kind: input.kind,
1377
- progress: text(input.progress, "汇报进展"), variance: optional(input.variance, "偏差"), nextStep: optional(input.nextStep, "下一步"), risks: strings(input.risks, "风险"),
1425
+ progress: text(input.progress, "汇报进展"), summary: typeof input.summary === "string" && input.summary.trim() ? text(input.summary, "汇报标题") : undefined, variance: optional(input.variance, "偏差"), nextStep: optional(input.nextStep, "下一步"), risks: strings(input.risks, "风险"),
1378
1426
  decisionNeeded: optional(input.decisionNeeded, "所需决策"), evidenceRefs: strings(input.evidenceRefs, "证据"), createdAt: stamp() };
1379
1427
  p.reports.push(report);
1380
1428
  if (report.kind === "blocked") {
@@ -1438,6 +1486,10 @@ export class ProjectService {
1438
1486
  projectId: id,
1439
1487
  taskId: t?.id,
1440
1488
  reason: text(input.reason, "升级原因"),
1489
+ /* THE QUESTION IS KEPT BESIDE THE CASE. The title is what the reader decides ON - one
1490
+ sentence, in the reader's terms - and the reason is the case for it. An escalation
1491
+ * without a title is still a valid escalation; it simply has to be read in full. */
1492
+ ...(typeof input.title === "string" && input.title.trim() ? { title: text(input.title, "升级标题") } : {}),
1441
1493
  severity: input.severity,
1442
1494
  status: "open",
1443
1495
  requestedByConversationId: actor,
@@ -1446,19 +1498,19 @@ export class ProjectService {
1446
1498
  createdAt: stamp(),
1447
1499
  };
1448
1500
  p.escalations.push(e);
1449
- this.event(p, "escalated", e.reason, t?.id, actor);
1501
+ this.event(p, "escalated", e.title ? `${e.title}:${e.reason}` : e.reason, t?.id, actor);
1450
1502
  this.reconcile(p);
1451
1503
  return e;
1452
1504
  }, actor);
1453
1505
  if (supervisorToWake) {
1454
- await this.wake(supervisorToWake, `下级成员(会话:${actor},身份:${wakeMemberRole ?? "执行者"})发起例外升级(ID:${result.id}):\n原因:${result.reason}\n严重级别:${result.severity}\n请及时处理该例外(调用 project action="resolve_escalation" 记录处理意见),或按需向上级转述升级。`, `例外升级(ID:${result.id})`, id, workspaceId, actor);
1506
+ await this.wake(supervisorToWake, `下级成员(会话:${actor},身份:${wakeMemberRole ?? "执行者"})发起例外升级(ID:${result.id}):\n${result.title ? `待决问题:${result.title}\n` : ""}原因:${result.reason}\n严重级别:${result.severity}\n请及时处理该例外(调用 project action="resolve_escalation" 记录处理意见),或按需向上级转述升级。`, `例外升级(ID:${result.id})`, id, workspaceId, actor);
1455
1507
  }
1456
1508
  return result;
1457
1509
  }
1458
1510
  async resolveEscalation(id, escalationId, decision, status, workspaceId, actor) {
1459
1511
  const result = await this.change(id, workspaceId, (p) => {
1460
1512
  this.live(p);
1461
- if (status !== "resolved" && status !== "dismissed")
1513
+ if (status !== "resolved" && status !== "dismissed" && status !== "returned")
1462
1514
  throw new Error("升级处理状态不合法");
1463
1515
  const e = p.escalations.find((e) => e.id === escalationId);
1464
1516
  if (!e)
@@ -1481,10 +1533,24 @@ export class ProjectService {
1481
1533
  e.decision = text(decision, "处理意见");
1482
1534
  e.resolvedAt = stamp();
1483
1535
  const decider = audience === "boss" ? "Boss" : "直接上级";
1484
- this.event(p, "decision", `${decider}决策:${e.decision}`, e.taskId, actor);
1536
+ /* A RETURN IS NOT A DECISION. It says the case was not ready - the reader is being asked
1537
+ * for more material, not for a ruling - so the timeline says so in those words. */
1538
+ this.event(p, "decision", status === "returned"
1539
+ ? `${decider}退回例外,等待补充材料:${e.decision}`
1540
+ : `${decider}决策:${e.decision}`, e.taskId, actor);
1485
1541
  this.reconcile(p);
1486
1542
  return e;
1487
1543
  }, actor);
1544
+ /* THE CASE GOES BACK TO WHO RAISED IT. A returned escalation leaves the inbox without being
1545
+ * answered, so nobody would ever know it was still open - unless the person who raised it is
1546
+ * woken up and told to bring the missing material. This is the one wake that is NOT about a
1547
+ * decision: it is about work coming back. */
1548
+ if (status === "returned") {
1549
+ if (result.requestedByConversationId) {
1550
+ await this.wake(result.requestedByConversationId, `例外已退回,等待对方补充材料(ID:${result.id}):${result.title ?? result.reason}\n退回意见:${result.decision}\n请补充材料后重新发起升级。`, `例外已退回(ID:${result.id})`, id, workspaceId, actor);
1551
+ }
1552
+ return result;
1553
+ }
1488
1554
  const p = this.raw(id, workspaceId);
1489
1555
  if (result.audience === "supervisor") {
1490
1556
  if (result.requestedByConversationId) {
@@ -9,7 +9,7 @@ import { join, resolve } from "node:path";
9
9
  import { pathToFileURL } from "node:url";
10
10
  import { openStateStore } from "./persistence/open-state-store.js";
11
11
  import { PiRpcRuntime, WelcomeRuntime } from "./runtime.js";
12
- import { resolveAgentDirectories } from "./agents.js";
12
+ import { agentModeOf, resolveAgentDirectories } from "./agents.js";
13
13
  import { releaseAllCapabilities } from "./capability.js";
14
14
  import { SubagentSupervisor } from "./subagents.js";
15
15
  import { SubagentRunRecorder } from "./subagent-runs.js";
@@ -297,7 +297,18 @@ runService.setExecService(execService);
297
297
  const scheduledTaskService = new ScheduledTaskService(store, runService, conversationService, broadcaster);
298
298
  const sessionLedger = new SessionLedger();
299
299
  const sessionOrchestratorService = new SessionOrchestratorService(store, conversationService, runService, sessionLedger, broadcaster);
300
- const projectService = new ProjectService(store, sessionOrchestratorService);
300
+ /**
301
+ * THE PROJECT SERVICE IS TOLD HOW TO ASK WHAT AN AGENT IS.
302
+ *
303
+ * The employee rule of §1 - "a subagent cannot be a project employee" - is a fact about the agent
304
+ * DIRECTORY, and the directory is read through the same reader the picker and the runtime use
305
+ * (`readAgents`, through `agentModeOf`). The directories are asked of the catalogue service
306
+ * because THEY are the live ones: the user can add an agents directory at any time, and a guard
307
+ * that read a path captured at startup would answer about a directory that is no longer there.
308
+ */
309
+ const projectService = new ProjectService(store, sessionOrchestratorService, {
310
+ agentModeOf: (agentId) => agentModeOf(catalogService.getAgentDirs(), agentId),
311
+ });
301
312
  void sessionOrchestratorService.restoreWatchdogs().catch((error) => {
302
313
  console.warn("Super Pi: restoring watchdogs failed:", error instanceof Error ? error.message : String(error));
303
314
  });
@@ -1011,6 +1011,7 @@ export type ProjectReport = {
1011
1011
  authorConversationId?: string;
1012
1012
  kind: "progress" | "blocked" | "delivery" | "decision_request" | "review";
1013
1013
  progress: string;
1014
+ summary?: string;
1014
1015
  variance?: string;
1015
1016
  nextStep?: string;
1016
1017
  risks?: string[];
@@ -1022,9 +1023,12 @@ export type ProjectEscalation = {
1022
1023
  id: string;
1023
1024
  projectId: string;
1024
1025
  taskId?: string;
1025
- reason: string;
1026
+ reason: string; /** THE QUESTION, IN ONE SENTENCE. What the Boss has to decide - as opposed to `reason`,
1027
+ * which is the case for it. The reader decides in a glance, and reads the case only if the
1028
+ * question is not obvious; an escalation without a title falls back to the reason. */
1029
+ title?: string;
1026
1030
  severity: ProjectRisk;
1027
- status: "open" | "resolved" | "dismissed";
1031
+ status: "open" | "resolved" | "dismissed" | "returned";
1028
1032
  requestedByConversationId?: string;
1029
1033
  decision?: string;
1030
1034
  createdAt: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.167",
3
+ "version": "0.1.168",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -196,6 +196,7 @@
196
196
  "scripts/session-state-icon.test.mjs",
197
197
  "scripts/provider-fetch-models.test.mjs",
198
198
  "scripts/shared-chrome.test.mjs",
199
+ "scripts/shared-chrome-recovery.test.mjs",
199
200
  "scripts/prompt-templates.test.mjs",
200
201
  "scripts/documents.test.mjs",
201
202
  "scripts/web-tools.test.mjs",
@@ -232,7 +233,8 @@
232
233
  "scripts/awaiting-background-status.test.mjs",
233
234
  "scripts/project-lifecycle-e2e.test.mjs",
234
235
  "scripts/agent-tiers.test.mjs",
235
- "scripts/design-principles.test.mjs"
236
+ "scripts/design-principles.test.mjs",
237
+ "scripts/primary-employee-guard.test.mjs"
236
238
  ],
237
239
  "check:browser": [
238
240
  "scripts/interrupt-browser.test.mjs",
@@ -266,8 +268,10 @@
266
268
  "scripts/workspace-identity-browser.test.mjs",
267
269
  "scripts/sidebar-tools-browser.test.mjs",
268
270
  "scripts/project-employee-panel-browser.test.mjs",
271
+ "scripts/primary-employee-guard-browser.test.mjs",
269
272
  "scripts/project-evidence-panel-browser.test.mjs",
270
273
  "scripts/project-panel-fixes-browser.test.mjs",
274
+ "scripts/boss-approval-readability-browser.test.mjs",
271
275
  "scripts/builtin-agents-project-browser.test.mjs",
272
276
  "scripts/tool-runtime-browser.test.mjs",
273
277
  "scripts/session-context-menu-browser.test.mjs",