@evandu/super-pi 0.1.156 → 0.1.158

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.
@@ -1135,7 +1135,7 @@ export async function executeScheduledTask(
1135
1135
  export const PROJECT_TOOL_NAME = "project";
1136
1136
  export const PROJECT_ENDPOINT_ENV = "SUPER_PI_PROJECT_ENDPOINT";
1137
1137
 
1138
- export const PROJECT_DESCRIPTION = `Manage a user-triggered project through accountable delegation, evidence and independent review. Read the latest state with get. The project leader uses add_task (title, objective, deliverables, acceptanceCriteria, dependencies, role=leader/executor, optional executorProfile/reviewerProfile with agentId, cwd, model, thinkingLevel, watchdogTimeoutSeconds), dispatch and request_review. A branch leader creates children with parentTaskId set to its own task ID and manages only its direct tasks. Dependencies must be siblings. Leader aggregate submission requires independently accepted children. Keep at most 3 leader levels and 8 branch leaders; small projects need only one executor. Workers submit output plus evidenceRefs. Only the assigned independent reviewer can review with verdict accepted or changes_requested, summary and evidenceRefs. update_task marks blocked/failed with blockedReason, updates executorProfile/reviewerProfile before child sessions spawn, or the leader resets ready for retry. report records kind=progress/blocked, progress, variance, nextStep, risks, decisionNeeded and evidenceRefs. escalate records reason and severity=low/medium/high for Boss decisions. complete requires all tasks independently accepted and a final summary with evidenceRefs. Project and task IDs come from get/add_task. Reads never invoke models. Dispatch is asynchronous and consumes budget; stop polling when capacity is full and wait for child completion reports. Do not use session_spawn, session_dispatch, task or workflow to bypass the project dispatch guard. Never claim success on an error.`;
1138
+ export const PROJECT_DESCRIPTION = `Manage a user-triggered project through accountable delegation, evidence and independent review. When the user describes a project in natural language, decompose it yourself into title, objective, deliverables, acceptanceCriteria and budget, then call create with those fields and the top-level tasks you derived. Create also accepts optional constraints, risk, budget with maxTokens/maxConcurrent, and default profiles defaultLeaderProfile/defaultExecutorProfile/defaultReviewerProfile. The tasks array carries that decomposition: each entry needs a title and an objective, and its dependencies name the titles of sibling tasks in the same array. The project starts on action="start" - call it only after you have registered the top-level tasks, because the leader session begins decomposing the work the moment it starts. You are the initiator: you may create the project, register top-level tasks and start it; you may not register child tasks, change the budget or cancel the scope. Only a root conversation may create a project. Top-level tasks can also be registered later with add_task. Read the latest state with get. The project leader uses add_task (title, objective, deliverables, acceptanceCriteria, dependencies, role=leader/executor, optional executorProfile/reviewerProfile with agentId, cwd, model, thinkingLevel, watchdogTimeoutSeconds), dispatch and request_review. A branch leader creates children with parentTaskId set to its own task ID and manages only its direct tasks. Dependencies must be siblings. Leader aggregate submission requires independently accepted children. Keep at most 3 leader levels and 8 branch leaders; small projects need only one executor. Workers submit output plus evidenceRefs. Only the assigned independent reviewer can review with verdict accepted or changes_requested, summary and evidenceRefs. update_task marks blocked/failed with blockedReason, updates executorProfile/reviewerProfile before child sessions spawn, or the leader resets ready for retry. report records kind=progress/blocked, progress, variance, nextStep, risks, decisionNeeded and evidenceRefs. escalate records reason and severity=low/medium/high for Boss decisions. complete requires all tasks independently accepted and a final summary with evidenceRefs. Project and task IDs come from create/get/add_task. Reads never invoke models. Dispatch is asynchronous and consumes budget; stop polling when capacity is full and wait for child completion reports. Do not use session_spawn, session_dispatch, task or workflow to bypass the project dispatch guard. Never claim success on an error.`;
1139
1139
 
1140
1140
  export function readProjectConfig(env: Record<string, string | undefined>) {
1141
1141
  return { endpoint: env[PROJECT_ENDPOINT_ENV] ?? "", conversationId: env[TASK_CONVERSATION_ENV] ?? "", workspaceId: env.SUPER_PI_ORCHESTRATOR_WORKSPACE ?? "", capability: env[TASK_CAPABILITY_ENV] };
@@ -305,15 +305,37 @@ const extension = (pi: ExtensionApi): void => {
305
305
  pi.registerTool({
306
306
  name: PROJECT_TOOL_NAME, label: "Project", description: PROJECT_DESCRIPTION,
307
307
  parameters: Type.Object({
308
- action: Type.Union(["get", "add_task", "dispatch", "submit", "request_review", "review", "update_task", "report", "escalate", "complete"].map((value) => Type.Literal(value))),
309
- projectId: Type.String(), taskId: Type.Optional(Type.String()),
308
+ action: Type.Union(["create", "start", "get", "add_task", "dispatch", "submit", "request_review", "review", "update_task", "report", "escalate", "complete"].map((value) => Type.Literal(value))),
309
+ projectId: Type.Optional(Type.String()), taskId: Type.Optional(Type.String()),
310
310
  parentTaskId: Type.Optional(Type.String()), role: Type.Optional(Type.Union([Type.Literal("leader"), Type.Literal("executor")])),
311
311
  title: Type.Optional(Type.String()), objective: Type.Optional(Type.String()),
312
- deliverables: Type.Optional(Type.Array(Type.String())), acceptanceCriteria: Type.Optional(Type.Array(Type.String())), dependencies: Type.Optional(Type.Array(Type.String())),
312
+ deliverables: Type.Optional(Type.Array(Type.String())), acceptanceCriteria: Type.Optional(Type.Array(Type.String())), constraints: Type.Optional(Type.Array(Type.String())), dependencies: Type.Optional(Type.Array(Type.String())),
313
+ risk: Type.Optional(Type.String()),
314
+ budget: Type.Optional(Type.Object({ maxTokens: Type.Optional(Type.Number()), maxConcurrent: Type.Optional(Type.Number()) })),
313
315
  priority: Type.Optional(Type.String()), status: Type.Optional(Type.String()), blockedReason: Type.Optional(Type.String()),
316
+ tasks: Type.Optional(Type.Array(Type.Object({
317
+ title: Type.String(), objective: Type.String(),
318
+ deliverables: Type.Optional(Type.Array(Type.String())), acceptanceCriteria: Type.Optional(Type.Array(Type.String())),
319
+ dependencies: Type.Optional(Type.Array(Type.String())), priority: Type.Optional(Type.String()),
320
+ }))),
314
321
  output: Type.Optional(Type.String()), evidenceRefs: Type.Optional(Type.Array(Type.String())), verdict: Type.Optional(Type.String()), summary: Type.Optional(Type.String()),
315
322
  kind: Type.Optional(Type.String()), progress: Type.Optional(Type.String()), variance: Type.Optional(Type.String()), nextStep: Type.Optional(Type.String()),
316
323
  risks: Type.Optional(Type.Array(Type.String())), decisionNeeded: Type.Optional(Type.String()), reason: Type.Optional(Type.String()), severity: Type.Optional(Type.String()),
324
+ defaultLeaderProfile: Type.Optional(Type.Object({
325
+ agentId: Type.Optional(Type.String()), cwd: Type.Optional(Type.String()),
326
+ model: Type.Optional(Type.Object({ provider: Type.String(), modelId: Type.String() })),
327
+ thinkingLevel: Type.Optional(Type.String()), watchdogTimeoutSeconds: Type.Optional(Type.Number()),
328
+ })),
329
+ defaultExecutorProfile: Type.Optional(Type.Object({
330
+ agentId: Type.Optional(Type.String()), cwd: Type.Optional(Type.String()),
331
+ model: Type.Optional(Type.Object({ provider: Type.String(), modelId: Type.String() })),
332
+ thinkingLevel: Type.Optional(Type.String()), watchdogTimeoutSeconds: Type.Optional(Type.Number()),
333
+ })),
334
+ defaultReviewerProfile: Type.Optional(Type.Object({
335
+ agentId: Type.Optional(Type.String()), cwd: Type.Optional(Type.String()),
336
+ model: Type.Optional(Type.Object({ provider: Type.String(), modelId: Type.String() })),
337
+ thinkingLevel: Type.Optional(Type.String()), watchdogTimeoutSeconds: Type.Optional(Type.Number()),
338
+ })),
317
339
  executorProfile: Type.Optional(Type.Object({
318
340
  agentId: Type.Optional(Type.String()), cwd: Type.Optional(Type.String()),
319
341
  model: Type.Optional(Type.Object({ provider: Type.String(), modelId: Type.String() })),
@@ -39,5 +39,5 @@
39
39
  "src/client/types.ts": "b4d24bfabf230e29b984cf698aeb579c19797139a2b358f1a4e61cafce02964d",
40
40
  "src/client/vite-env.d.ts": "65996936fbb042915f7b74a200fcdde7e410f32a669b1ab9597cfaa4b0faddb5"
41
41
  },
42
- "recordedAt": "2026-11-03T12:20:19.321Z"
42
+ "recordedAt": "2026-11-04T07:47:46.546Z"
43
43
  }
@@ -19,6 +19,7 @@ export type CreateProjectInput = {
19
19
  defaultLeaderProfile?: unknown;
20
20
  defaultExecutorProfile?: unknown;
21
21
  defaultReviewerProfile?: unknown;
22
+ initiatorConversationId?: string;
22
23
  };
23
24
  export type AddTaskInput = Partial<ProjectTask> & {
24
25
  title: string;
@@ -26,6 +27,15 @@ export type AddTaskInput = Partial<ProjectTask> & {
26
27
  executorProfile?: unknown;
27
28
  reviewerProfile?: unknown;
28
29
  };
30
+ /** A top-level task an initiator hands over with create, before the service gives it an ID. */
31
+ export type TopLevelTaskPlan = {
32
+ title: string;
33
+ objective: string;
34
+ deliverables?: string[];
35
+ acceptanceCriteria?: string[];
36
+ priority?: ProjectTask["priority"];
37
+ dependencies: string[];
38
+ };
29
39
  /** Persist explicit domain fields only; transport credentials never enter project records. */
30
40
  export declare class ProjectService {
31
41
  private readonly store;
@@ -40,6 +50,7 @@ export declare class ProjectService {
40
50
  private change;
41
51
  private live;
42
52
  private leader;
53
+ private topLevelAuthor;
43
54
  private manager;
44
55
  private requiredComplete;
45
56
  private task;
@@ -49,7 +60,7 @@ export declare class ProjectService {
49
60
  list(workspaceId?: string): Project[];
50
61
  get(id: string, workspaceId?: string): Project;
51
62
  projectForSession(conversationId: string): Project | undefined;
52
- create(input: CreateProjectInput): Promise<Project>;
63
+ create(input: CreateProjectInput, initiatorConversationId?: string): Promise<Project>;
53
64
  private admission;
54
65
  private context;
55
66
  start(id: string, workspaceId: string, actor?: string): Promise<Project>;
@@ -77,5 +88,13 @@ export declare class ProjectService {
77
88
  addEscalation(id: string, input: Pick<ProjectEscalation, "reason" | "severity" | "taskId">, workspaceId?: string, actor?: string): Promise<ProjectEscalation>;
78
89
  resolveEscalation(id: string, escalationId: string, decision: string, status: "resolved" | "dismissed", workspaceId?: string): Promise<ProjectEscalation>;
79
90
  complete(id: string, summary: string, evidenceRefs: unknown, workspaceId: string, actor?: string): Promise<Project>;
91
+ /**
92
+ * The tasks an initiator hands over with create. A dependency names the TITLE of another task in
93
+ * the same batch, because the batch carries no task IDs yet - the service assigns them while it
94
+ * registers the batch. The plan comes back in dependency order, so the plain add_task rules
95
+ * (title, objective, priority, sibling dependencies) can be reused unchanged.
96
+ */
97
+ private planTopLevelTasks;
98
+ private registerTopLevelTasks;
80
99
  tool(body: Record<string, any>, workspaceId: string, actor: string): Promise<unknown>;
81
100
  }
@@ -177,6 +177,15 @@ export class ProjectService {
177
177
  throw new Error("项目已结束,不能继续修改或派发"); }
178
178
  leader(p, actor) { if (actor && actor !== p.ownerConversationId)
179
179
  throw new Error("仅项目负责人可以执行此操作"); }
180
+ topLevelAuthor(p, actor) {
181
+ if (!actor)
182
+ return;
183
+ if (actor === p.ownerConversationId)
184
+ return;
185
+ if (actor === p.initiatorConversationId)
186
+ return;
187
+ throw new Error("只有项目负责人或发起人可以登记顶层任务");
188
+ }
180
189
  manager(p, t, actor) {
181
190
  const manager = t.parentTaskId ? this.task(p, t.parentTaskId).ownerConversationId : p.ownerConversationId;
182
191
  if (actor && actor !== manager)
@@ -194,7 +203,7 @@ export class ProjectService {
194
203
  return t;
195
204
  }
196
205
  member(p, actor) {
197
- if (actor && actor !== p.ownerConversationId && !p.tasks.some((t) => t.ownerConversationId === actor || t.reviewerConversationId === actor))
206
+ if (actor && actor !== p.ownerConversationId && actor !== p.initiatorConversationId && !p.tasks.some((t) => t.ownerConversationId === actor || t.reviewerConversationId === actor))
198
207
  throw new Error("调用会话不属于项目执行团队");
199
208
  }
200
209
  reconcile(p) {
@@ -250,7 +259,7 @@ export class ProjectService {
250
259
  }
251
260
  return undefined;
252
261
  }
253
- async create(input) {
262
+ async create(input, initiatorConversationId) {
254
263
  const workspaceId = this.workspace(input.workspaceId);
255
264
  if (input.ownerConversationId !== undefined)
256
265
  throw new Error("项目负责人由启动操作创建独立会话");
@@ -258,12 +267,16 @@ export class ProjectService {
258
267
  throw new Error("项目风险等级不合法");
259
268
  if (input.budget !== undefined && (!input.budget || typeof input.budget !== "object" || Array.isArray(input.budget)))
260
269
  throw new Error("资源预算不合法");
261
- for (const [key, value] of Object.entries(input.budget ?? {}))
270
+ for (const [key, value] of Object.entries(input.budget ?? {})) {
271
+ if (value === undefined)
272
+ continue;
262
273
  if (!["maxTokens", "maxConcurrent"].includes(key) || !Number.isSafeInteger(value) || value <= 0)
263
274
  throw new Error("资源预算必须是正整数");
275
+ }
264
276
  const defaultLeaderProfile = profile(input.defaultLeaderProfile, "默认 Leader 配置");
265
277
  const defaultExecutorProfile = profile(input.defaultExecutorProfile, "默认执行配置");
266
278
  const defaultReviewerProfile = profile(input.defaultReviewerProfile, "默认复核配置");
279
+ const initiator = (initiatorConversationId ?? (typeof input.initiatorConversationId === "string" ? input.initiatorConversationId : undefined))?.trim() || undefined;
267
280
  const now = stamp();
268
281
  const p = { id: `project_${randomUUID()}`, workspaceId, title: text(input.title, "项目标题"), objective: text(input.objective, "项目目标"),
269
282
  deliverables: strings(input.deliverables, "交付物"), acceptanceCriteria: strings(input.acceptanceCriteria, "验收标准"), constraints: strings(input.constraints, "约束"),
@@ -271,8 +284,14 @@ export class ProjectService {
271
284
  ...(defaultLeaderProfile ? { defaultLeaderProfile } : {}),
272
285
  ...(defaultExecutorProfile ? { defaultExecutorProfile } : {}),
273
286
  ...(defaultReviewerProfile ? { defaultReviewerProfile } : {}),
287
+ ...(initiator ? { initiatorConversationId: initiator } : {}),
274
288
  tasks: [], reports: [], escalations: [], events: [], createdAt: now, updatedAt: now };
275
- this.event(p, "created", "Boss 触发项目");
289
+ if (initiator) {
290
+ this.event(p, "created", "Agent 触发项目", undefined, initiator);
291
+ }
292
+ else {
293
+ this.event(p, "created", "Boss 触发项目");
294
+ }
276
295
  const saved = await this.store.setProject(p);
277
296
  return this.projectView(saved);
278
297
  }
@@ -303,7 +322,7 @@ export class ProjectService {
303
322
  }
304
323
  async start(id, workspaceId, actor) {
305
324
  return this.change(id, workspaceId, async (p) => {
306
- this.leader(p, actor);
325
+ this.topLevelAuthor(p, actor);
307
326
  this.admission(p, false);
308
327
  if (!this.orchestrator)
309
328
  throw new Error("会话编排服务未就绪");
@@ -334,7 +353,9 @@ export class ProjectService {
334
353
  return this.change(id, workspaceId, (p) => {
335
354
  this.live(p);
336
355
  const parentTaskId = optional(input.parentTaskId, "父任务 id");
337
- let manager = p.ownerConversationId;
356
+ // A top-level task is accountable to whoever owns the project: the leader once it runs, and
357
+ // before that the initiator who registered it - a task nobody answers for cannot be closed.
358
+ let manager = p.ownerConversationId ?? p.initiatorConversationId;
338
359
  if (parentTaskId) {
339
360
  const parent = this.task(p, parentTaskId);
340
361
  if (parent.role !== "leader" || parent.status !== "in_progress" || !parent.ownerConversationId)
@@ -352,7 +373,7 @@ export class ProjectService {
352
373
  throw new Error("Leader 最多 3 层,请改为执行任务");
353
374
  }
354
375
  else
355
- this.leader(p, actor);
376
+ this.topLevelAuthor(p, actor);
356
377
  if (input.role !== undefined && input.role !== "leader" && input.role !== "executor")
357
378
  throw new Error("任务角色必须是 leader 或 executor");
358
379
  const dependencies = strings(input.dependencies, "任务依赖");
@@ -723,12 +744,109 @@ export class ProjectService {
723
744
  return p;
724
745
  });
725
746
  }
747
+ /**
748
+ * The tasks an initiator hands over with create. A dependency names the TITLE of another task in
749
+ * the same batch, because the batch carries no task IDs yet - the service assigns them while it
750
+ * registers the batch. The plan comes back in dependency order, so the plain add_task rules
751
+ * (title, objective, priority, sibling dependencies) can be reused unchanged.
752
+ */
753
+ planTopLevelTasks(batch) {
754
+ if (batch === undefined || batch === null)
755
+ return [];
756
+ if (!Array.isArray(batch))
757
+ throw new Error("tasks 必须是任务数组");
758
+ if (batch.length > 100)
759
+ throw new Error("项目最多 100 个任务,请收敛分工范围");
760
+ const items = batch.map((raw) => {
761
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
762
+ throw new Error("tasks 的每一项必须是任务对象");
763
+ const record = raw;
764
+ const priority = record.priority;
765
+ if (priority !== undefined && !["low", "medium", "high", "urgent"].includes(String(priority)))
766
+ throw new Error("任务优先级不合法");
767
+ return {
768
+ title: text(record.title, "任务标题"),
769
+ objective: text(record.objective, "任务目标"),
770
+ deliverables: record.deliverables === undefined ? undefined : strings(record.deliverables, "任务交付物"),
771
+ acceptanceCriteria: record.acceptanceCriteria === undefined ? undefined : strings(record.acceptanceCriteria, "任务验收标准"),
772
+ priority: priority,
773
+ dependencies: record.dependencies === undefined ? [] : strings(record.dependencies, "任务依赖"),
774
+ };
775
+ });
776
+ const index = new Map();
777
+ items.forEach((item, at) => {
778
+ if (index.has(item.title))
779
+ throw new Error(`任务标题重复:${item.title}`);
780
+ index.set(item.title, at);
781
+ });
782
+ const edges = items.map((item) => item.dependencies.map((dependency) => {
783
+ const at = index.get(dependency);
784
+ if (at === undefined)
785
+ throw new Error(`任务依赖不存在:${dependency}`);
786
+ return at;
787
+ }));
788
+ const ordered = [];
789
+ const done = new Set();
790
+ while (ordered.length < items.length) {
791
+ let progressed = false;
792
+ for (let at = 0; at < items.length; at += 1) {
793
+ if (done.has(at))
794
+ continue;
795
+ if (!edges[at].every((dependency) => done.has(dependency)))
796
+ continue;
797
+ ordered.push(items[at]);
798
+ done.add(at);
799
+ progressed = true;
800
+ }
801
+ if (!progressed)
802
+ throw new Error("任务依赖存在循环,无法登记顶层任务");
803
+ }
804
+ return ordered;
805
+ }
806
+ async registerTopLevelTasks(projectId, plan, workspaceId, actor) {
807
+ const ids = new Map();
808
+ for (const item of plan) {
809
+ const dependencies = item.dependencies.map((dependency) => ids.get(dependency));
810
+ const task = await this.addTask(projectId, {
811
+ title: item.title, objective: item.objective,
812
+ ...(item.deliverables ? { deliverables: item.deliverables } : {}),
813
+ ...(item.acceptanceCriteria ? { acceptanceCriteria: item.acceptanceCriteria } : {}),
814
+ ...(item.priority !== undefined ? { priority: item.priority } : {}),
815
+ dependencies,
816
+ }, workspaceId, actor);
817
+ ids.set(item.title, task.id);
818
+ }
819
+ }
726
820
  async tool(body, workspaceId, actor) {
821
+ if (body.action === "create") {
822
+ // ONLY A ROOT CONVERSATION MAY OPEN A PROJECT. Every dispatched session - the leader, its
823
+ // reviewers, the workers - holds the same tool, and a project opened from one of them would
824
+ // carry its own budget and concurrency guards while sitting outside the guard that dispatched it.
825
+ const conversation = this.store.conversation(actor);
826
+ if (!conversation)
827
+ throw new Error("找不到这个会话。");
828
+ if (conversation.parentConversationId)
829
+ throw new Error("只有主对话可以创建项目:派发出来的会话不能另起项目,否则会绕过项目的预算与并发护栏。");
830
+ // AND A SESSION THAT IS ALREADY IN A PROJECT MAY NOT OPEN ANOTHER ONE. The leader session is
831
+ // spawned without a parent, so it passes the root-conversation gate above, yet a project it
832
+ // opens would carry a fresh budget and concurrency cap outside the guard that dispatched it.
833
+ if (this.projectForSession(actor))
834
+ throw new Error("已经在项目里的会话不能另起项目:再开一个会绕开当前项目的预算与并发护栏。");
835
+ const running = this.store.projects().filter((p) => p.initiatorConversationId === actor && !closed(p));
836
+ if (running.length >= 3)
837
+ throw new Error("你同时进行的项目超过上限(3 个),先完成或停止一个再新建。");
838
+ const { tasks: batch, ...input } = body;
839
+ const plan = this.planTopLevelTasks(batch);
840
+ const project = await this.create({ ...input, workspaceId }, actor);
841
+ await this.registerTopLevelTasks(project.id, plan, workspaceId, actor);
842
+ return this.get(project.id, workspaceId);
843
+ }
727
844
  const id = text(body.projectId, "projectId");
728
845
  const p = this.get(id, workspaceId);
729
846
  this.member(p, actor);
730
847
  switch (body.action) {
731
848
  case "get": return p;
849
+ case "start": return this.start(id, workspaceId, actor);
732
850
  case "add_task": return this.addTask(id, body, workspaceId, actor);
733
851
  case "dispatch": return this.dispatchTask(id, text(body.taskId, "taskId"), workspaceId, actor);
734
852
  case "submit": return this.submit(id, text(body.taskId, "taskId"), body, workspaceId, actor);
@@ -996,6 +996,9 @@ export type Project = {
996
996
  risk: ProjectRisk;
997
997
  status: ProjectStatus;
998
998
  ownerConversationId?: string;
999
+ /** The session that created the project - the Boss's own conversation, which may register the
1000
+ * top-level tasks and is the one the project answers to. */
1001
+ initiatorConversationId?: string;
999
1002
  budget?: {
1000
1003
  maxTokens?: number;
1001
1004
  maxConcurrent?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.156",
3
+ "version": "0.1.158",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",