@bf-meishijian/aip 0.0.5 → 0.0.6

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 (53) hide show
  1. package/README.md +2 -2
  2. package/dist/coding-run-manager.d.ts +8 -2
  3. package/dist/coding-run-manager.d.ts.map +1 -1
  4. package/dist/coding-run-manager.js +406 -300
  5. package/dist/coding-run-manager.js.map +1 -1
  6. package/dist/coding-subagent-child.d.ts +22 -0
  7. package/dist/coding-subagent-child.d.ts.map +1 -0
  8. package/dist/coding-subagent-child.js +90 -0
  9. package/dist/coding-subagent-child.js.map +1 -0
  10. package/dist/coding-subagents.d.ts +63 -0
  11. package/dist/coding-subagents.d.ts.map +1 -0
  12. package/dist/coding-subagents.js +537 -0
  13. package/dist/coding-subagents.js.map +1 -0
  14. package/dist/command-wakeup.d.ts +19 -0
  15. package/dist/command-wakeup.d.ts.map +1 -0
  16. package/dist/command-wakeup.js +109 -0
  17. package/dist/command-wakeup.js.map +1 -0
  18. package/dist/daemon.d.ts.map +1 -1
  19. package/dist/daemon.js +68 -43
  20. package/dist/daemon.js.map +1 -1
  21. package/dist/delivery-files.d.ts +12 -0
  22. package/dist/delivery-files.d.ts.map +1 -1
  23. package/dist/delivery-files.js +80 -0
  24. package/dist/delivery-files.js.map +1 -1
  25. package/dist/live-employee.d.ts +4 -2
  26. package/dist/live-employee.d.ts.map +1 -1
  27. package/dist/live-employee.js +47 -13
  28. package/dist/live-employee.js.map +1 -1
  29. package/dist/pi-message-queue.d.ts +15 -0
  30. package/dist/pi-message-queue.d.ts.map +1 -0
  31. package/dist/pi-message-queue.js +57 -0
  32. package/dist/pi-message-queue.js.map +1 -0
  33. package/dist/pi-model-service.d.ts.map +1 -1
  34. package/dist/platform-client.d.ts +16 -0
  35. package/dist/platform-client.d.ts.map +1 -1
  36. package/dist/platform-client.js +21 -0
  37. package/dist/platform-client.js.map +1 -1
  38. package/dist/review-report.d.ts +10 -0
  39. package/dist/review-report.d.ts.map +1 -0
  40. package/dist/review-report.js +59 -0
  41. package/dist/review-report.js.map +1 -0
  42. package/dist/stage-delivery.js +3 -2
  43. package/dist/state.d.ts +2 -2
  44. package/dist/state.js +2 -2
  45. package/package.json +11 -7
  46. package/dist/agent-harness/index.d.ts +0 -4
  47. package/dist/agent-harness/index.d.ts.map +0 -1
  48. package/dist/agent-harness/index.js +0 -4
  49. package/dist/agent-harness/index.js.map +0 -1
  50. package/dist/agent-harness/pi-chat-model.d.ts +0 -35
  51. package/dist/agent-harness/pi-chat-model.d.ts.map +0 -1
  52. package/dist/agent-harness/pi-chat-model.js +0 -285
  53. package/dist/agent-harness/pi-chat-model.js.map +0 -1
@@ -6,11 +6,14 @@ import { basename, dirname, join, resolve } from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  import { createAgentSession, DefaultResourceLoader, SessionManager, } from "@earendil-works/pi-coding-agent";
8
8
  import { Type } from "typebox";
9
- import { collectDeliveryFiles, deliveryDirectory, initializeDeliveryFiles, } from "./delivery-files.js";
9
+ import { createCodingSubagents } from "./coding-subagents.js";
10
+ import { collectDeliveryFiles, deliveryDirectory, initializeDeliveryFiles, publishReviewMaterials, } from "./delivery-files.js";
10
11
  import { liveEmployee } from "./live-employee.js";
11
12
  import { prepareMergeRequest, registerMergeRequest, } from "./merge-requests.js";
12
13
  import { paths } from "./paths.js";
14
+ import { PiMessageQueue } from "./pi-message-queue.js";
13
15
  import { PlatformClientError } from "./platform-client.js";
16
+ import { materializeReviewReport } from "./review-report.js";
14
17
  import { readJson, writeSecureJson } from "./secure-json.js";
15
18
  import { DELIVERY_FILES, STAGE_DELIVERY_PROTOCOLS, stageEmployeeIds } from "./stage-delivery.js";
16
19
  const execFileAsync = promisify(execFile);
@@ -27,6 +30,25 @@ export class CodingRunManager {
27
30
  async execute(client, host, command) {
28
31
  if (!command.run)
29
32
  throw new Error("CODING_RUN_COMMAND_INVALID");
33
+ if (command.run.action === "REVIEW_REPORT") {
34
+ const report = command.run.reviewReport;
35
+ if (!report || !command.run.runInput)
36
+ throw new Error("REVIEW_REPORT_INPUT_MISSING");
37
+ const files = await materializeReviewReport(join(paths().runWorktrees, command.run.runId), command.run.runInput, report.files);
38
+ await client.acknowledgeReviewReport(command.run.runId, {
39
+ hostId: host.hostId,
40
+ generation: host.generation,
41
+ turnId: command.run.turnId,
42
+ roundId: report.roundId,
43
+ files,
44
+ });
45
+ return;
46
+ }
47
+ if (command.run.action === "RECALL_QUEUE") {
48
+ return {
49
+ recalledMessageIds: this.active.get(command.run.runId)?.observed.messageQueue?.recall() ?? [],
50
+ };
51
+ }
30
52
  const imageRun = command.run;
31
53
  let images;
32
54
  try {
@@ -41,6 +63,7 @@ export class CodingRunManager {
41
63
  if (active?.turnId === imageRun.turnId) {
42
64
  active.stopping = true;
43
65
  active.observed.stopping = true;
66
+ await active.subagents?.stop();
44
67
  await active.session.abort();
45
68
  await active.finished;
46
69
  if (this.active.get(imageRun.runId) === active)
@@ -52,7 +75,18 @@ export class CodingRunManager {
52
75
  const input = command.run.runInput;
53
76
  await this.start(client, host, command.run.runId, command.run.turnId, input, false, command.run.contentMarkdown, images);
54
77
  }
55
- else if (command.run.action === "STEER") {
78
+ else if (command.run.action === "CONTINUE") {
79
+ const active = this.active.get(command.run.runId);
80
+ if (active) {
81
+ await active.finished;
82
+ if (active.stopping || active.observed.userIntervened)
83
+ return;
84
+ }
85
+ if (!command.run.runInput || !command.run.contentMarkdown || !command.run.continuation)
86
+ throw new Error("CODING_RUN_CONTINUATION_MISSING");
87
+ await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, [], command.run.continuation);
88
+ }
89
+ else if (command.run.action === "STEER" || command.run.action === "FOLLOW_UP") {
56
90
  let active = this.active.get(command.run.runId);
57
91
  if (!command.run.contentMarkdown && images.length === 0)
58
92
  throw new Error("CODING_RUN_MESSAGE_MISSING");
@@ -61,6 +95,13 @@ export class CodingRunManager {
61
95
  throw new Error("CODING_RUN_TURN_MISMATCH");
62
96
  active.stopping = true;
63
97
  active.observed.stopping = true;
98
+ await active.emit("EXECUTION_DECIDED", {
99
+ phase: active.input.phase,
100
+ decision: "SKIP",
101
+ reason: "SUPERSEDED",
102
+ stopReason: "SUPERSEDED",
103
+ });
104
+ await active.subagents?.stop();
64
105
  await active.session.abort();
65
106
  if (this.active.get(command.run.runId) === active)
66
107
  this.active.delete(command.run.runId);
@@ -69,17 +110,48 @@ export class CodingRunManager {
69
110
  if (active) {
70
111
  if (images.length > 0 && !active.supportsImages)
71
112
  throw new Error("MODEL_IMAGES_UNSUPPORTED");
72
- active.observed.pendingSteerText = command.run.contentMarkdown ?? "";
73
- await active.emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
74
- if (images.length > 0)
75
- await active.session.steer(command.run.contentMarkdown ?? "", images);
76
- else
77
- await active.session.steer(command.run.contentMarkdown ?? "");
113
+ active.observed.userIntervened = true;
114
+ const handoff = active.observed.awaitingDecision || active.session.isIdle;
115
+ if (handoff)
116
+ active.observed.handingOffInput = true;
117
+ await active.emit("AUTO_CONTINUATION_CHANGED", {
118
+ phase: active.input.phase,
119
+ autoContinueEnabled: false,
120
+ reason: "USER_MESSAGE",
121
+ source: "HOST_HANDLED",
122
+ });
123
+ if (handoff || active.session.isIdle || active.observed.awaitingDecision) {
124
+ active.observed.handingOffInput = true;
125
+ // Pi is idle: steer only queues input and cannot start a new model turn.
126
+ // Finish the old judge operation before opening the user turn.
127
+ await active.finished;
128
+ if (!command.run.runInput)
129
+ throw new Error("CODING_RUN_INPUT_MISSING");
130
+ await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
131
+ if (command.run.messageId)
132
+ await this.active.get(command.run.runId)?.emit("MESSAGE_QUEUE_UPDATED", {
133
+ messageId: command.run.messageId,
134
+ deliveryStatus: "READ",
135
+ });
136
+ return;
137
+ }
138
+ if (!command.run.messageId)
139
+ throw new Error("CODING_RUN_MESSAGE_ID_MISSING");
140
+ await active.observed.messageQueue.submit(command.run.messageId, command.run.action, command.run.contentMarkdown ?? "", images.length ? images : undefined);
141
+ await active.emit("MESSAGE_QUEUE_UPDATED", {
142
+ messageId: command.run.messageId,
143
+ deliveryStatus: "QUEUED",
144
+ });
78
145
  }
79
146
  else {
80
147
  if (!command.run.runInput)
81
148
  throw new Error("CODING_RUN_INPUT_MISSING");
82
149
  await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
150
+ if (command.run.messageId)
151
+ await this.active.get(command.run.runId)?.emit("MESSAGE_QUEUE_UPDATED", {
152
+ messageId: command.run.messageId,
153
+ deliveryStatus: "READ",
154
+ });
83
155
  }
84
156
  }
85
157
  else if (command.run.action === "STOP") {
@@ -89,34 +161,55 @@ export class CodingRunManager {
89
161
  throw new Error("CODING_RUN_TURN_MISMATCH");
90
162
  active.stopping = true;
91
163
  active.observed.stopping = true;
164
+ await active.subagents?.stop();
92
165
  await active.session.abort();
166
+ await active.emit("AUTO_CONTINUATION_CHANGED", {
167
+ phase: active.input.phase,
168
+ autoContinueEnabled: false,
169
+ reason: "USER_STOP",
170
+ source: "HOST_HANDLED",
171
+ });
93
172
  await active.emit.flush();
94
- for (const [activityKey, agentId] of active.observed.activeTasks) {
95
- await active.emit("SUBAGENT_STOPPED", {
96
- activityKey,
97
- agentId,
98
- summary: "随主执行停止",
99
- });
100
- }
101
- active.observed.activeTasks.clear();
102
- await active.emit("RUN_STOPPED", {});
173
+ await active.emit("EXECUTION_DECIDED", {
174
+ phase: active.input.phase,
175
+ decision: "SKIP",
176
+ reason: "STOP_REQUESTED",
177
+ stopReason: "USER_STOPPED",
178
+ });
179
+ await active.emit("RUN_STOPPED", { phase: active.input.phase, reason: "USER_STOPPED" });
103
180
  }
104
181
  else {
105
182
  const stateFile = join(paths().runState, `${command.run.runId}.json`);
106
183
  const state = await readJson(stateFile);
107
- await eventEmitter(client, host, command.run.runId, command.run.turnId, state?.lastEventSequence ?? 0, (sequence) => writeSecureJson(stateFile, { ...state, lastEventSequence: sequence }), (value) => redactHostText(value, [paths().root]))("RUN_STOPPED", {});
184
+ await eventEmitter(client, host, command.run.runId, command.run.turnId, state?.lastEventSequence ?? 0, (sequence) => writeSecureJson(stateFile, { ...state, lastEventSequence: sequence }), (value) => redactHostText(value, [paths().root]))("RUN_STOPPED", {
185
+ phase: state?.phase,
186
+ reason: "USER_STOPPED",
187
+ reminderDecision: "NO_REMINDER",
188
+ });
108
189
  }
109
190
  }
110
191
  else if (command.run.action === "RESUME") {
111
192
  await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
112
193
  }
194
+ if (command.run.messageId &&
195
+ ["START", "RESUME", "STEER", "FOLLOW_UP"].includes(command.run.action)) {
196
+ const active = this.active.get(command.run.runId);
197
+ if (active &&
198
+ (command.run.action === "RESUME" ||
199
+ command.run.action === "START" ||
200
+ !active.observed.messageQueue))
201
+ await active.emit("MESSAGE_QUEUE_UPDATED", {
202
+ messageId: command.run.messageId,
203
+ deliveryStatus: "READ",
204
+ });
205
+ }
113
206
  }
114
- async start(client, host, runId, turnId, input, resume, resumePrompt, images = []) {
207
+ async start(client, host, runId, turnId, input, resume, resumePrompt, images = [], continuation) {
115
208
  const active = this.active.get(runId);
116
209
  if (active) {
117
210
  if (active.turnId === turnId)
118
211
  return;
119
- if (!active.observed.terminalReached)
212
+ if (!active.observed.terminalReached && !active.stopping)
120
213
  throw new Error("CODING_RUN_TURN_MISMATCH");
121
214
  await active.finished;
122
215
  if (this.active.get(runId) === active)
@@ -135,9 +228,19 @@ export class CodingRunManager {
135
228
  Object.keys(storedState.phaseSessions).length &&
136
229
  storedState.harness !== "pi")
137
230
  throw new Error("PRIOR_HARNESS_SESSION_UNSUPPORTED:请新建执行,历史文件和工作目录已保留。");
138
- const cwd = await prepareProjects(local.repositoryCache, runRoot, runId, input.projects, Boolean(storedState?.prepared));
139
231
  for (const project of input.projects)
140
- project.devBranch ||= `aip/${runId}/${project.id.slice(0, 8)}`;
232
+ project.devBranch ||=
233
+ storedState?.mrContext?.projects.find((p) => p.id === project.id)?.devBranch ||
234
+ `aip/${input.workItem.key}`;
235
+ const cwd = await prepareProjects(local.repositoryCache, runRoot, input.workItem.key, input.projects, Boolean(storedState?.prepared));
236
+ const preparationBaselines = storedState?.preparationBaselines ?? {};
237
+ if (input.phase === "PREPARATION" && !storedState?.prepared) {
238
+ for (const project of input.projects)
239
+ preparationBaselines[project.id] ??= (await execFileAsync("git", ["rev-parse", "HEAD"], {
240
+ cwd: join(runRoot, safeName(project.name)),
241
+ })).stdout.trim();
242
+ await writeSecureJson(stateFile, { ...storedState, preparationBaselines });
243
+ }
141
244
  if (!storedState?.prepared || input.inputConflict)
142
245
  await initializeDeliveryFiles(runRoot, input, Boolean(storedState?.prepared));
143
246
  const deliveryInputHash = createHash("sha256")
@@ -147,7 +250,7 @@ export class CodingRunManager {
147
250
  input.technicalDesign?.content,
148
251
  ]))
149
252
  .digest("hex");
150
- const childSessions = storedState?.childSessions ?? {};
253
+ const delegations = storedState?.delegations ?? {};
151
254
  let pendingDelivery = storedState?.phase !== input.phase ||
152
255
  resumePrompt ||
153
256
  images.length > 0 ||
@@ -166,8 +269,7 @@ export class CodingRunManager {
166
269
  let sessionRef = phaseSessions[sessionPhase];
167
270
  let lastEventSequence = storedState?.lastEventSequence ?? 0;
168
271
  const observed = {
169
- pendingQuestion: storedState?.phase === input.phase ? (storedState.pendingQuestion ?? "") : "",
170
- memberResults: new Map(),
272
+ userIntervened: input.autoContinueEnabled === false,
171
273
  activeTasks: new Map(),
172
274
  stopping: false,
173
275
  terminalReached: false,
@@ -182,12 +284,12 @@ export class CodingRunManager {
182
284
  primaryProjectId: input.primaryProjectId,
183
285
  },
184
286
  prepared: true,
287
+ preparationBaselines,
185
288
  phaseSessions,
186
289
  sessionRef,
187
290
  harness: "pi",
188
291
  lastEventSequence,
189
- pendingQuestion: observed.pendingQuestion,
190
- childSessions,
292
+ delegations,
191
293
  pendingDelivery,
192
294
  deliveryInputHash,
193
295
  });
@@ -197,15 +299,15 @@ export class CodingRunManager {
197
299
  return persistState();
198
300
  }, redact);
199
301
  if (input.phase === "PREPARATION") {
302
+ if (!storedState?.prepared)
303
+ await publishReviewMaterials(runRoot, input);
200
304
  await persistState();
201
305
  await emit("PREPARATION_COMPLETED", {
202
306
  primaryProjectId: input.primaryProjectId,
203
307
  projects: await Promise.all(input.projects.map(async (p) => ({
204
308
  projectId: p.id,
205
309
  projectName: p.name,
206
- baselineCommit: (await execFileAsync("git", ["rev-parse", "HEAD"], {
207
- cwd: join(runRoot, safeName(p.name)),
208
- })).stdout.trim(),
310
+ baselineCommit: preparationBaselines[p.id] ?? p.baselineCommit,
209
311
  directory: join(runRoot, safeName(p.name)),
210
312
  branch: (await execFileAsync("git", ["branch", "--show-current"], {
211
313
  cwd: join(runRoot, safeName(p.name)),
@@ -214,10 +316,10 @@ export class CodingRunManager {
214
316
  });
215
317
  return;
216
318
  }
217
- const terminal = terminalTools(client, host, runId, turnId, runRoot, input, observed, redact, async (batch) => {
319
+ const terminal = terminalTools(client, host, runId, turnId, runRoot, input, observed, async (batch) => {
218
320
  pendingDelivery = batch;
219
321
  await persistState();
220
- }, () => pendingDelivery, (question) => emit("QUESTION_UPDATED", { question: redact(question) }), () => emit.flush());
322
+ }, () => pendingDelivery, () => emit.flush());
221
323
  if (!this.pi.resolveRuntimeModel)
222
324
  throw new Error("MODEL_RUNTIME_UNAVAILABLE");
223
325
  const resolved = await this.pi.resolveRuntimeModel(input.providerId, input.modelId, {
@@ -235,119 +337,41 @@ export class CodingRunManager {
235
337
  member: "supervisor",
236
338
  };
237
339
  const availableAgents = stageEmployeeIds(input);
238
- const live = liveEmployee(client, runId, binding, await client.employeeConfig(runId, { ...binding, purpose: "LOAD" }), runRoot, availableAgents.length > 0, input.executionPurpose === "RELEASE", input.phase, () => observed.terminalReached);
239
- const childInstances = new Map();
240
- const childUpdates = new Map();
241
- const childTasks = new Map();
242
- const memberExtension = (api) => {
243
- if (!availableAgents.length)
244
- return;
245
- api.registerTool({
246
- name: "subagent",
247
- label: "派发研发团队",
248
- description: `向实际绑定员工分派子任务,返回真实子会话结果。可用员工:${availableAgents
249
- .map((id) => `${id}:${input.teamEmployees?.[id]?.name ?? id};${input.teamEmployees?.[id]?.description ?? ""}`)
250
- .join("\n")}。`,
251
- parameters: Type.Object({
252
- agent: availableAgents.length === 1
253
- ? Type.Literal(availableAgents[0])
254
- : Type.Union(availableAgents.map((name) => Type.Literal(name))),
255
- task: Type.String({ minLength: 1 }),
256
- }),
257
- async execute(callId, params, signal, onUpdate) {
258
- const member = String(params.agent);
259
- if (!availableAgents.includes(member))
260
- throw new Error("DEVELOPMENT_TEAM_MEMBER_INVALID");
261
- if (observed.stopping || signal?.aborted)
262
- throw new Error("SUBAGENT_STOPPED");
263
- observed.activeTasks.set(callId, member);
264
- childTasks.set(member, callId);
265
- observed.memberResults.delete(member);
266
- childUpdates.set(member, (text) => onUpdate?.({ content: [{ type: "text", text }], details: { agent: member } }));
267
- const key = `${input.phase}:${member}`;
268
- let child = childInstances.get(member);
269
- if (!child) {
270
- const childBinding = { ...binding, member };
271
- const childLive = liveEmployee(client, runId, childBinding, await client.employeeConfig(runId, { ...childBinding, purpose: "LOAD" }), runRoot, true, false, input.phase, () => observed.memberResults.has(member));
272
- const resultExtension = (pi) => {
273
- pi.registerTool({
274
- name: "submit_member_result",
275
- label: "提交成员结果",
276
- description: "如实记录本次子任务结果;未解决的问题必须 FAIL。仅向主管汇报,不确认最终交付。",
277
- parameters: Type.Object({
278
- status: Type.Union([Type.Literal("PASS"), Type.Literal("FAIL")]),
279
- summary: Type.String({ minLength: 1 }),
280
- }),
281
- async execute(_id, result) {
282
- const revisions = await captureProjectRevisions(runRoot, input.projects, input.workItem.key, input.primaryProjectId);
283
- observed.memberResults.set(member, { ...result, revisions });
284
- queueMicrotask(() => child?.agent.abort());
285
- return toolResult("结果已记录,向主管返回结论。");
286
- },
287
- });
288
- };
289
- const opened = await openPiSession({
290
- cwd,
291
- sessionDir: local.piSessions,
292
- agentDir: local.piAgent,
293
- existingId: childSessions[key],
294
- modelRuntime: resolved.runtime,
295
- model: resolved.model,
296
- thinkingLevel: input.thinkingLevel,
297
- extensions: [childLive.extension, resultExtension],
298
- systemPrompt: "当前为团队子成员会话,员工配置由平台下发。",
299
- runId,
300
- });
301
- childLive.attach(opened.session);
302
- child = opened.session;
303
- childSessions[key] = opened.sessionId;
304
- childInstances.set(member, child);
305
- subscribeSession(child, (kind, payload) => {
306
- if (kind === "TEXT_DELTA")
307
- childUpdates.get(member)?.(String(payload.text ?? ""));
308
- return emit(kind, {
309
- ...payload,
310
- phase: input.phase,
311
- agentId: member,
312
- parentActivityKey: childTasks.get(member),
313
- sessionId: opened.sessionId,
314
- });
315
- }, observed, redact);
316
- await persistState();
317
- }
318
- await emit("SUBAGENT_UPDATED", {
319
- phase: input.phase,
320
- task: params.task,
321
- activityKey: callId,
322
- agentId: member,
323
- sessionRef: childSessions[key],
324
- summary: "独立子会话已就绪",
325
- });
326
- const abort = () => {
327
- child?.agent.abort();
328
- };
329
- signal?.addEventListener("abort", abort, { once: true });
330
- try {
331
- signal?.throwIfAborted();
332
- await child.prompt(`${initialPrompt(input, runRoot, false)}\n\n本次主管分派给 ${member} 的任务:${params.task}\n只完成自身职责,最后调用 submit_member_result;主管负责其他成员协调和最终交付。`);
333
- await child.waitForIdle();
334
- if (signal?.aborted || observed.stopping)
335
- throw new Error("SUBAGENT_STOPPED");
336
- const result = observed.memberResults.get(member);
337
- if (!result)
338
- throw new Error("SUBAGENT_RESULT_MISSING");
339
- return {
340
- content: [{ type: "text", text: JSON.stringify(result) }],
341
- details: { agent: member, status: result.status },
342
- };
343
- }
344
- finally {
345
- childUpdates.delete(member);
346
- signal?.removeEventListener("abort", abort);
347
- }
340
+ const live = liveEmployee(client, runId, binding, await client.employeeConfig(runId, { ...binding, purpose: "LOAD" }), runRoot, Boolean(input.team), input.executionPurpose === "RELEASE", input.phase, () => observed.terminalReached, executionInstructions, () => {
341
+ observed.terminalClosed = true;
342
+ });
343
+ const delegation = input.executionPurpose === "RELEASE"
344
+ ? undefined
345
+ : await createCodingSubagents({
346
+ client,
347
+ config: {
348
+ runId,
349
+ turnId,
350
+ hostId: host.hostId,
351
+ generation: host.generation,
352
+ runRoot,
353
+ providerId: input.providerId,
354
+ modelId: input.modelId,
355
+ phase: input.phase,
356
+ },
357
+ cwd,
358
+ directory: join(local.runState, runId, "subagents", input.phase),
359
+ team: Boolean(input.team),
360
+ agents: availableAgents.map((id) => ({
361
+ id,
362
+ name: input.teamEmployees?.[id]?.name ?? id,
363
+ description: input.teamEmployees?.[id]?.description ?? "",
364
+ })),
365
+ records: delegations,
366
+ emit,
367
+ persist: persistState,
368
+ started(activityKey, agentId) {
369
+ observed.activeTasks.set(activityKey, agentId);
370
+ },
371
+ async completed(activityKey) {
372
+ observed.activeTasks.delete(activityKey);
348
373
  },
349
374
  });
350
- };
351
375
  const opened = await openPiSession({
352
376
  cwd,
353
377
  sessionDir: local.piSessions,
@@ -356,7 +380,7 @@ export class CodingRunManager {
356
380
  modelRuntime: resolved.runtime,
357
381
  model: resolved.model,
358
382
  thinkingLevel: input.thinkingLevel,
359
- extensions: [...(availableAgents.length ? [memberExtension] : []), terminal, live.extension],
383
+ extensions: [...(delegation ? [delegation.extension] : []), terminal, live.extension],
360
384
  systemPrompt: input.team
361
385
  ? "当前为团队主负责人会话,员工配置由平台下发。"
362
386
  : "当前为工单主会话,员工配置由平台下发。",
@@ -365,6 +389,7 @@ export class CodingRunManager {
365
389
  live.attach(opened.session);
366
390
  const session = opened.session;
367
391
  sessionRef = opened.sessionId;
392
+ observed.messageQueue = new PiMessageQueue(session);
368
393
  subscribeSession(session, (kind, payload) => {
369
394
  const activityKey = String(payload.activityKey ?? "");
370
395
  if (kind === "SUBAGENT_STARTED")
@@ -386,29 +411,62 @@ export class CodingRunManager {
386
411
  emit,
387
412
  stopping: false,
388
413
  observed,
414
+ ...(delegation ? { subagents: delegation } : {}),
389
415
  };
390
416
  this.active.set(runId, activeRun);
391
417
  phaseSessions[sessionPhase] = sessionRef;
392
418
  await persistState();
393
419
  await emit("RUN_STARTED", { sessionRef, phase: input.phase });
394
- if (resumePrompt || images.length > 0) {
420
+ if (!continuation &&
421
+ (input.autoContinueEnabled === false || (!resume && input.autoContinueEnabled === true))) {
422
+ await emit("AUTO_CONTINUATION_CHANGED", {
423
+ phase: input.phase,
424
+ autoContinueEnabled: input.autoContinueEnabled === true,
425
+ reason: input.autoContinueEnabled === true ? "SYSTEM_STAGE" : "USER_MESSAGE",
426
+ source: "HOST_HANDLED",
427
+ });
428
+ }
429
+ if (!continuation && (resumePrompt || images.length > 0)) {
395
430
  await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
396
431
  await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
397
432
  }
398
- const prompt = `${initialPrompt(input, runRoot)}${resumePrompt ? `\n\n${resumePrompt}` : resume ? "\n\n继续当前会话未完成的工作,先读取最新文件和交接结果。" : ""}`;
399
- activeRun.finished = driveToTerminal(session, prompt, observed, client, host, runId, turnId, emit, input.executionPurpose === "RELEASE", input.phase, images)
433
+ const conversationFingerprint = await workspaceFingerprint(runRoot, input);
434
+ const prompt = continuation
435
+ ? resumePrompt
436
+ : `${initialPrompt(input, runRoot)}\n\n${executionInstructions()}${resumePrompt ? `\n\n用户本次消息:${resumePrompt}` : resume && !resumePrompt ? "\n\n继续当前会话未完成的工作,先读取最新文件和交接结果。" : ""}`;
437
+ activeRun.finished = driveToTerminal(session, prompt, observed, client, host, runId, turnId, emit, input.phase, images, async () => {
438
+ await emit("CONVERSATION_COMPLETED", {
439
+ workspaceChanged: conversationFingerprint !== (await workspaceFingerprint(runRoot, input)),
440
+ });
441
+ }, continuation)
400
442
  .catch(async (error) => {
401
443
  if (activeRun.stopping)
402
444
  return;
445
+ await emit("EXECUTION_DECIDED", {
446
+ phase: input.phase,
447
+ decision: "SKIP",
448
+ reason: "MODEL_ERROR",
449
+ stopReason: "error",
450
+ });
403
451
  await emit("RUN_FAILED", {
404
- code: error instanceof Error ? error.message.slice(0, 200) : "PI_RUN_FAILED",
452
+ phase: input.phase,
453
+ code: error instanceof Error && /^[A-Z][A-Z0-9_]+$/u.test(error.message)
454
+ ? error.message.slice(0, 200)
455
+ : "PI_RUN_FAILED",
456
+ message: redact(error instanceof Error ? error.message : String(error)),
405
457
  });
406
458
  })
407
459
  .finally(async () => {
408
460
  await emit.flush();
409
- for (const child of childInstances.values())
410
- child.dispose();
411
- session.dispose();
461
+ try {
462
+ await delegation?.stop();
463
+ await delegation?.flush();
464
+ }
465
+ finally {
466
+ await delegation?.dispose();
467
+ await session.extensionRunner?.emit({ type: "session_shutdown", reason: "quit" });
468
+ session.dispose();
469
+ }
412
470
  if (this.active.get(runId) === activeRun)
413
471
  this.active.delete(runId);
414
472
  });
@@ -461,7 +519,9 @@ function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistS
461
519
  let sequence = initialSequence;
462
520
  let queue = Promise.resolve();
463
521
  const emit = (kind, payload) => {
464
- queue = queue.then(async () => {
522
+ queue = queue
523
+ .catch(() => { })
524
+ .then(async () => {
465
525
  const event = {
466
526
  eventId: crypto.randomUUID(),
467
527
  sequence: sequence + 1,
@@ -484,11 +544,16 @@ function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistS
484
544
  catch (error) {
485
545
  if (error instanceof PlatformClientError &&
486
546
  error.status === 409 &&
487
- error.code === "CODING_EVENT_TURN_STALE")
547
+ error.code === "CODING_EVENT_TURN_STALE") {
548
+ if (["EXECUTION_STARTED", "EXECUTION_PAUSED", "CONVERSATION_COMPLETED"].includes(kind))
549
+ throw error;
488
550
  return;
551
+ }
489
552
  process.stderr.write(`[local-coding-host] 事件上报失败:${redact(error instanceof Error ? error.message : String(error))}\n`);
490
553
  if (attempt === EVENT_APPEND_MAX_ATTEMPTS) {
491
554
  process.stderr.write(`[local-coding-host] 事件上报放弃:${event.kind} sequence=${event.sequence} attempts=${attempt}\n`);
555
+ if (["EXECUTION_STARTED", "EXECUTION_PAUSED", "CONVERSATION_COMPLETED"].includes(kind))
556
+ throw error;
492
557
  return;
493
558
  }
494
559
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -558,6 +623,7 @@ async function openPiSession(options) {
558
623
  resourceLoader: loader,
559
624
  sessionManager,
560
625
  });
626
+ await session.bindExtensions({ mode: "print" });
561
627
  session.setAutoCompactionEnabled(true);
562
628
  return { session, sessionId };
563
629
  }
@@ -588,17 +654,22 @@ function subscribeSession(session, emit, observed, redact) {
588
654
  void emit(update.kind, update.payload);
589
655
  };
590
656
  session.subscribe((event) => {
591
- if (event.type === "message_start" && event.message.role === "assistant") {
657
+ if (event.type === "agent_start") {
658
+ if (observed.messageQueue)
659
+ void emit("MESSAGE_QUEUE_UPDATED", { mainAgentRunning: true });
660
+ }
661
+ else if (event.type === "agent_settled") {
662
+ if (observed.messageQueue)
663
+ void emit("MESSAGE_QUEUE_UPDATED", { mainAgentRunning: false });
664
+ }
665
+ else if (event.type === "message_start" && event.message.role === "assistant") {
592
666
  messageId = crypto.randomUUID();
593
667
  void emit("MESSAGE_STARTED", { messageId });
594
668
  }
595
669
  else if (event.type === "message_start" && event.message.role === "user") {
596
- const text = textFromMessage(event.message.content);
597
- if (observed.pendingSteerText !== undefined &&
598
- (!observed.pendingSteerText || text.includes(observed.pendingSteerText))) {
599
- delete observed.pendingSteerText;
600
- void emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
601
- }
670
+ const id = observed.messageQueue?.consumed(event.message);
671
+ if (id)
672
+ void emit("MESSAGE_QUEUE_UPDATED", { messageId: id, deliveryStatus: "READ" });
602
673
  }
603
674
  else if (event.type === "agent_end") {
604
675
  observed.nativeWillRetry = event.willRetry === true;
@@ -619,65 +690,38 @@ function subscribeSession(session, emit, observed, redact) {
619
690
  }
620
691
  else if (event.type === "tool_execution_start") {
621
692
  flushText();
622
- const subagent = event.toolName === "subagent";
623
- if (subagent)
624
- observed.activeTasks.set(event.toolCallId, String(event.args?.agent));
625
- void emit(subagent ? "SUBAGENT_STARTED" : "TOOL_STARTED", subagent
626
- ? {
627
- activityKey: event.toolCallId,
628
- agentId: String(event.args?.agent ?? ""),
629
- summary: String(event.args?.task ?? "团队子任务开始"),
630
- task: String(event.args?.task ?? ""),
631
- }
632
- : {
633
- activityKey: event.toolCallId,
634
- toolName: event.toolName,
635
- summary: `执行 ${event.toolName}`,
636
- });
693
+ if (event.toolName === "subagent")
694
+ return;
695
+ void emit("TOOL_STARTED", {
696
+ activityKey: event.toolCallId,
697
+ toolName: event.toolName,
698
+ summary: `执行 ${event.toolName}`,
699
+ });
637
700
  }
638
701
  else if (event.type === "tool_execution_update") {
702
+ if (event.toolName === "subagent")
703
+ return;
639
704
  pendingToolUpdates.set(event.toolCallId, {
640
- kind: event.toolName === "subagent" ? "SUBAGENT_UPDATED" : "TOOL_UPDATED",
641
- payload: event.toolName === "subagent"
642
- ? {
643
- activityKey: event.toolCallId,
644
- agentId: String(event.args?.agent ?? observed.activeTasks.get(event.toolCallId) ?? ""),
645
- currentAction: redact(textFromResult(event.partialResult)),
646
- }
647
- : {
648
- activityKey: event.toolCallId,
649
- toolName: event.toolName,
650
- currentAction: redact(textFromResult(event.partialResult)),
651
- },
705
+ kind: "TOOL_UPDATED",
706
+ payload: {
707
+ activityKey: event.toolCallId,
708
+ toolName: event.toolName,
709
+ currentAction: redact(textFromResult(event.partialResult)),
710
+ },
652
711
  });
653
712
  if (!toolUpdateTimers.has(event.toolCallId))
654
713
  toolUpdateTimers.set(event.toolCallId, setTimeout(() => flushToolUpdate(event.toolCallId), 200));
655
714
  }
656
715
  else if (event.type === "tool_execution_end") {
657
- flushToolUpdate(event.toolCallId);
658
- const member = observed.activeTasks.get(event.toolCallId);
659
716
  if (event.toolName === "subagent")
660
- observed.activeTasks.delete(event.toolCallId);
661
- void emit(event.toolName === "subagent"
662
- ? observed.stopping
663
- ? "SUBAGENT_STOPPED"
664
- : event.isError
665
- ? "SUBAGENT_FAILED"
666
- : "SUBAGENT_COMPLETED"
667
- : "TOOL_COMPLETED", event.toolName === "subagent"
668
- ? {
669
- activityKey: event.toolCallId,
670
- agentId: String(event.result?.details?.agent ??
671
- member ??
672
- ""),
673
- summary: redact(textFromResult(event.result)),
674
- }
675
- : {
676
- activityKey: event.toolCallId,
677
- toolName: event.toolName,
678
- isError: event.isError === true,
679
- summary: redact(textFromResult(event.result)),
680
- });
717
+ return;
718
+ flushToolUpdate(event.toolCallId);
719
+ void emit("TOOL_COMPLETED", {
720
+ activityKey: event.toolCallId,
721
+ toolName: event.toolName,
722
+ isError: event.isError === true,
723
+ summary: redact(textFromResult(event.result)),
724
+ });
681
725
  }
682
726
  else if (event.type === "bash_execution_update" && event.delta) {
683
727
  const activityKey = event.id ?? "bash";
@@ -750,60 +794,119 @@ function textFromResult(value) {
750
794
  return "";
751
795
  return textFromMessage(value.content);
752
796
  }
753
- async function driveToTerminal(session, initial, observed, client, host, runId, turnId, emit, release, phase, images = []) {
754
- let prompt = initial;
755
- for (let attempt = 0; attempt <= 2; attempt += 1) {
756
- if (!session.model?.input.includes("image") &&
757
- session.messages?.some((message) => "content" in message &&
758
- Array.isArray(message.content) &&
759
- message.content.some((block) => block.type === "image")))
760
- throw new Error("MODEL_IMAGES_UNSUPPORTED");
761
- if (attempt === 0 && images.length > 0)
762
- await session.prompt(prompt, { images });
763
- else
764
- await session.prompt(prompt);
765
- await session.waitForIdle();
766
- if (observed.stopping || observed.terminalReached || observed.pendingQuestion)
767
- return;
768
- if (observed.lastModelStopReason === "error" && observed.nativeWillRetry !== true) {
769
- const code = observed.lastModelError?.split("\n")[0]?.slice(0, 200) || "MODEL_REQUEST_FAILED";
770
- await client.reportCodingTerminal(runId, {
771
- hostId: host.hostId,
772
- generation: host.generation,
773
- turnId,
774
- kind: "FAILED",
775
- summary: observed.lastModelError ?? code,
776
- knownIssues: "",
777
- failureCode: code,
778
- projectFacts: [],
779
- testFacts: [],
780
- });
781
- observed.terminalReached = true;
782
- await emit("RUN_FAILED", { code });
797
+ async function driveToTerminal(session, initial, observed, client, host, runId, turnId, emit, phase, images = [], completeConversation = async () => { }, continuation) {
798
+ if (!session.model?.input.includes("image") &&
799
+ session.messages?.some((message) => "content" in message &&
800
+ Array.isArray(message.content) &&
801
+ message.content.some((block) => block.type === "image")))
802
+ throw new Error("MODEL_IMAGES_UNSUPPORTED");
803
+ if (observed.stopping)
804
+ return;
805
+ if (continuation && observed.userIntervened)
806
+ return;
807
+ const execution = images.length > 0 ? session.prompt(initial, { images }) : session.prompt(initial);
808
+ // Observe a synchronous rejection while the invocation fact is persisted.
809
+ const outcome = execution.then(() => ({ ok: true }), (error) => ({ ok: false, error }));
810
+ if (continuation) {
811
+ await emit("EXECUTION_CONTINUED", {
812
+ phase,
813
+ ...continuation,
814
+ prompt: initial,
815
+ deliveryStatus: "SUBMITTED",
816
+ });
817
+ }
818
+ const result = await outcome;
819
+ if (!result.ok)
820
+ throw result.error;
821
+ await session.waitForIdle();
822
+ observed.awaitingDecision = true;
823
+ if (observed.stopping)
824
+ return;
825
+ const stopReason = observed.terminalClosed
826
+ ? "TERMINAL_CLOSED"
827
+ : (observed.lastModelStopReason ?? "idle");
828
+ const endFact = { phase, stopReason, nativeStopReason: observed.lastModelStopReason ?? null };
829
+ if (observed.terminalReached) {
830
+ await emit("EXECUTION_DECIDED", {
831
+ ...endFact,
832
+ decision: "SKIP",
833
+ reason: "TERMINAL_ACCEPTED",
834
+ model: null,
835
+ });
836
+ return;
837
+ }
838
+ if (observed.lastModelStopReason === "aborted" ||
839
+ (observed.lastModelStopReason === "error" && observed.nativeWillRetry !== true)) {
840
+ const code = observed.lastModelError?.split("\n")[0]?.slice(0, 200) ||
841
+ (observed.lastModelStopReason === "aborted" ? "PI_MODEL_ABORTED" : "MODEL_REQUEST_FAILED");
842
+ await emit("EXECUTION_DECIDED", {
843
+ ...endFact,
844
+ decision: "SKIP",
845
+ reason: "MODEL_ERROR",
846
+ model: null,
847
+ });
848
+ if (observed.stopping)
783
849
  return;
784
- }
785
- if (attempt < 2) {
786
- prompt = release
787
- ? `请调用 submit_pull_requests 完成提 PR;最近错误:${observed.lastDeliveryError ?? "尚未提交"}。`
788
- : `你尚未成功交付。自主补齐必需产物后调用 ${phase === "DEVELOPMENT" ? "complete_development" : "submit_delivery"}。最近提交结果:${observed.lastDeliveryError ?? "尚未提交"}。缺文件就在当前会话补齐,传输故障不要重写报告。无法继续时说明真实原因,需要沟通可记录待回应问题。`;
789
- continue;
790
- }
791
850
  await client.reportCodingTerminal(runId, {
792
- hostId: host.hostId,
793
- generation: host.generation,
851
+ ...host,
794
852
  turnId,
795
853
  kind: "FAILED",
796
- summary: observed.lastDeliveryError ?? "Agent 多次结束但没有形成明确终态",
854
+ summary: observed.lastModelError ?? code,
797
855
  knownIssues: "",
798
- failureCode: observed.lastDeliveryError?.split("\n")[0]?.slice(0, 200) ?? "PI_TERMINAL_TOOL_NOT_CALLED",
856
+ failureCode: code,
799
857
  projectFacts: [],
800
858
  testFacts: [],
801
859
  });
802
860
  observed.terminalReached = true;
803
- await emit("RUN_FAILED", { code: "PI_TERMINAL_TOOL_NOT_CALLED" });
861
+ await emit("RUN_FAILED", { phase, code, message: observed.lastModelError ?? code });
862
+ return;
804
863
  }
864
+ // Judge reads durable messages. It queues automatic work through the same command
865
+ // claim boundary that gives user messages and stop priority.
866
+ await emit.flush();
867
+ if (observed.stopping)
868
+ return;
869
+ let decision;
870
+ try {
871
+ decision = await client.evaluateContinuation(runId, {
872
+ hostId: host.hostId,
873
+ generation: host.generation,
874
+ turnId,
875
+ stopReason,
876
+ ...(observed.lastDeliveryError
877
+ ? { lastDeliveryError: observed.lastDeliveryError.slice(0, 2000) }
878
+ : {}),
879
+ });
880
+ }
881
+ catch (error) {
882
+ if (observed.stopping)
883
+ return;
884
+ await emit("EXECUTION_DECIDED", {
885
+ ...endFact,
886
+ decision: "SKIP",
887
+ reason: "JUDGE_REQUEST_FAILED",
888
+ model: null,
889
+ explanation: error instanceof Error ? error.message : String(error),
890
+ });
891
+ if (observed.handingOffInput)
892
+ return;
893
+ await completeConversation();
894
+ observed.terminalReached = true;
895
+ return;
896
+ }
897
+ if (observed.stopping)
898
+ return;
899
+ await emit("EXECUTION_DECIDED", { ...endFact, ...decision });
900
+ // The platform has queued the continuation; never execute the returned prompt
901
+ // directly, because a user can cancel it before the host claims the command.
902
+ if (decision.decision === "CONTINUE")
903
+ return;
904
+ if (observed.stopping || observed.handingOffInput)
905
+ return;
906
+ await completeConversation();
907
+ observed.terminalReached = true;
805
908
  }
806
- function terminalTools(client, host, runId, turnId, runRoot, input, observed, redact, persistBatch, pendingBatch, emitQuestion, flushEvents) {
909
+ function terminalTools(client, host, runId, turnId, runRoot, input, observed, persistBatch, pendingBatch, flushEvents) {
807
910
  const definitions = [];
808
911
  const add = (definition) => {
809
912
  definitions.push(definition);
@@ -836,6 +939,8 @@ function terminalTools(client, host, runId, turnId, runRoot, input, observed, re
836
939
  : Type.Object({}),
837
940
  async execute(_toolCallId, rawParams) {
838
941
  try {
942
+ if (observed.activeTasks.size > 0)
943
+ throw new Error("SUBAGENT_TASKS_RUNNING:子任务尚未结束,请等待结果后再提交阶段交付。");
839
944
  if (input.executionPurpose === "RELEASE") {
840
945
  const params = rawParams;
841
946
  if (!params?.checklistSummary?.trim())
@@ -898,6 +1003,8 @@ function terminalTools(client, host, runId, turnId, runRoot, input, observed, re
898
1003
  ? Object.keys(DELIVERY_FILES).filter((slot) => STAGE_DELIVERY_PROTOCOLS.DEVELOPMENT.requiredFiles.includes(DELIVERY_FILES[slot]))
899
1004
  : undefined);
900
1005
  const projectFacts = await collectProjectFacts(runRoot, input.projects, input.workItem.key, input.primaryProjectId);
1006
+ if (input.phase === "DEVELOPMENT" && input.qualityReview?.enabled)
1007
+ await verifyReviewBranches(runRoot, input.projects, projectFacts);
901
1008
  const batch = {
902
1009
  hostId: host.hostId,
903
1010
  generation: host.generation,
@@ -925,18 +1032,6 @@ function terminalTools(client, host, runId, turnId, runRoot, input, observed, re
925
1032
  }
926
1033
  },
927
1034
  });
928
- add({
929
- name: "update_question",
930
- label: "更新待回应问题",
931
- description: "仅记录当前执行必须由人决定的业务问题,不把一般建议变为审批;真正解决后传空字符串清除。普通聊天继续使用 steer,不停止 Run。",
932
- parameters: Type.Object({ question: Type.String() }),
933
- async execute(_id, params) {
934
- // The caller binding supplies technical identity; the model only owns the question text.
935
- observed.pendingQuestion = params.question.trim();
936
- await emitQuestion(params.question);
937
- return toolResult(params.question);
938
- },
939
- });
940
1035
  if (input.executionPurpose === "RELEASE")
941
1036
  add({
942
1037
  name: "request_code_changes",
@@ -959,36 +1054,6 @@ function terminalTools(client, host, runId, turnId, runRoot, input, observed, re
959
1054
  return toolResult("已返回开发,等待下一执行接手续改,完成后进入测试与人工确认。");
960
1055
  },
961
1056
  });
962
- add({
963
- name: "report_blocker",
964
- label: "报告阻塞",
965
- description: "仅在认证、权限、仓库、外部环境或输入冲突真实阻塞时报告。",
966
- parameters: Type.Object({
967
- category: Type.Union([
968
- Type.Literal("AUTHENTICATION"),
969
- Type.Literal("PERMISSION"),
970
- Type.Literal("REPOSITORY"),
971
- Type.Literal("EXTERNAL_ENVIRONMENT"),
972
- Type.Literal("INPUT_CONFLICT"),
973
- ]),
974
- summary: Type.String(),
975
- }),
976
- async execute(_id, params) {
977
- await client.reportCodingTerminal(runId, {
978
- hostId: host.hostId,
979
- generation: host.generation,
980
- turnId,
981
- kind: "BLOCKED",
982
- summary: redact(params.summary),
983
- knownIssues: "",
984
- blockerCategory: params.category,
985
- projectFacts: [],
986
- testFacts: [],
987
- });
988
- observed.terminalReached = true;
989
- return toolResult("阻塞已报告。");
990
- },
991
- });
992
1057
  return (api) => {
993
1058
  for (const definition of definitions)
994
1059
  api.registerTool({
@@ -1003,7 +1068,7 @@ function terminalTools(client, host, runId, turnId, runRoot, input, observed, re
1003
1068
  });
1004
1069
  };
1005
1070
  }
1006
- export async function prepareProjects(cacheRoot, runRoot, runId, projects, resume) {
1071
+ export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects, resume) {
1007
1072
  await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
1008
1073
  for (const project of projects) {
1009
1074
  const mirror = join(cacheRoot, `${project.id}.git`);
@@ -1047,7 +1112,7 @@ export async function prepareProjects(cacheRoot, runRoot, runId, projects, resum
1047
1112
  });
1048
1113
  }
1049
1114
  const target = join(runRoot, safeName(project.name));
1050
- const expectedBranch = project.devBranch || `aip/${runId}/${project.id.slice(0, 8)}`;
1115
+ const expectedBranch = project.devBranch || `aip/${workItemKey}`;
1051
1116
  const existingWorktree = await exists(target);
1052
1117
  if (!existingWorktree) {
1053
1118
  if (resume)
@@ -1107,22 +1172,40 @@ async function canonicalRepository(value) {
1107
1172
  }
1108
1173
  return value.replace(/\/$/u, "");
1109
1174
  }
1175
+ function executionInstructions() {
1176
+ return "按当前任务和用户最新要求工作:自主选择工具,读取、修改、编译、测试和验证,完成阶段目标后调用提交工具。咨询、解释和有限操作按用户范围处理,不自动扩张为旧阶段任务。需要用户决定或外部信息时,用自然语言说明具体问题和已完成工作,不虚构答案、测试或交付成功。错误依据真实反馈修正;结果未知的有副作用操作先核实,不盲目重放。普通回复不等于交付,提交校验失败需要检查和修正,业务门禁仍须遵守。";
1177
+ }
1178
+ async function workspaceFingerprint(runRoot, input) {
1179
+ return JSON.stringify(await Promise.all(input.projects.map(async (project) => {
1180
+ const cwd = join(runRoot, safeName(project.name));
1181
+ const [head, branch, tree] = await Promise.all([
1182
+ execFileAsync("git", ["rev-parse", "HEAD"], { cwd }),
1183
+ execFileAsync("git", ["symbolic-ref", "--short", "-q", "HEAD"], { cwd }),
1184
+ worktreeTreeHash(cwd),
1185
+ ]);
1186
+ return [project.id, head.stdout.trim(), branch.stdout.trim(), tree];
1187
+ })));
1188
+ }
1110
1189
  export function initialPrompt(input, runRoot, responsible = true) {
1111
1190
  const directory = deliveryDirectory(runRoot, input);
1112
1191
  if (input.executionPurpose === "RELEASE")
1113
- return `当前是人工验收通过后的上线准备,恢复原开发会话。工单 ${input.workItem.key}:${input.workItem.title}。项目分支:${input.projects.map((p) => `${p.name} ${p.devBranch} → ${p.baselineBranch}`).join(";")}。共享工作区已由测试会话处理,不能依赖旧会话记忆:先读取最新代码、${directory}/report.md、release_checklist.md、summary.md 和适用的 design.md。以项目源分支和目标分支为依据,不按 commit ID 或 tree hash 校验验收一致性。核实上线清单,明确无需修改或修改摘要;清单覆盖项目与分支、SQL、Apollo、定时任务、Kafka/RabbitMQ资源、监控告警、发布顺序、验证和回滚。以设计为预期、代码核实;没有设计则从代码提取。无法证明的环境事实标明待核实,不涉及说明原因;测试环境部署记录不能代替上线清单。用户明确要求修改应用代码时调用 request_code_changes,返回开发→测试→人工确认,不能在此阶段擅自修改后放行。清单有缺口时使用 update_question 说明,不能宣称核实完成。核实完成后提交清单文档改动,再调用 submit_pull_requests,传入核实摘要和各项目 MR 标题说明;工具使用 Runtime 的 glab 身份推送、创建或复用 MR 并结构化登记工单。说明覆盖任务目的、本项目改动、测试验收、发布注意、跨项目依赖、风险回滚及材料入口。MR仍打开时复用;关闭后仅在用户明确重提时创建新MR。平台查询状态,不负责创建。仅说明或更新文档不退回研发;不执行实际发布、知识整理、关单。`;
1192
+ return `当前是人工验收通过后的上线准备,恢复原开发会话。工单 ${input.workItem.key}:${input.workItem.title}。项目分支:${input.projects.map((p) => `${p.name} ${p.devBranch} → ${p.baselineBranch}`).join(";")}。共享工作区已由测试会话处理,不能依赖旧会话记忆:先读取最新代码、${directory}/report.md、release_checklist.md、summary.md 和适用的 design.md。以项目源分支和目标分支为依据,不按 commit ID 或 tree hash 校验验收一致性。核实上线清单,明确无需修改或修改摘要;清单覆盖项目与分支、SQL、Apollo、定时任务、Kafka/RabbitMQ资源、监控告警、发布顺序、验证和回滚。以设计为预期、代码核实;没有设计则从代码提取。无法证明的环境事实标明待核实,不涉及说明原因;测试环境部署记录不能代替上线清单。用户明确要求修改应用代码时调用 request_code_changes,返回开发→测试→人工确认,不能在此阶段擅自修改后放行。清单有缺口时自然语言说明具体问题,需要用户补充时直接提问,不能宣称核实完成。核实完成后提交清单文档改动,再调用 submit_pull_requests,传入核实摘要和各项目 MR 标题说明;工具使用 Runtime 的 glab 身份推送、创建或复用 MR 并结构化登记工单。说明覆盖任务目的、本项目改动、测试验收、发布注意、跨项目依赖、风险回滚及材料入口。MR仍打开时复用;关闭后仅在用户明确重提时创建新MR。平台查询状态,不负责创建。仅说明或更新文档不退回研发;不执行实际发布、知识整理、关单。`;
1114
1193
  const projects = input.projects
1115
1194
  .map((p) => `- ${p.name}: ${join(runRoot, safeName(p.name))}(开发分支 ${p.devBranch} → 基准分支 ${p.baselineBranch};测试分支 ${p.testBranch})${p.id === input.primaryProjectId ? "(主项目)" : ""}`)
1116
1195
  .join("\n");
1117
1196
  const phase = input.phase === "DEVELOPMENT" ? "DEVELOPMENT" : "INTEGRATION";
1197
+ const phaseLabel = phase === "DEVELOPMENT" ? "开发与自审" : "系统测试,当前已进入系统测试,不是开发阶段";
1118
1198
  const protocol = STAGE_DELIVERY_PROTOCOLS[phase];
1119
1199
  const delivery = input.stageDeliverySnapshots?.[phase];
1120
1200
  if (responsible && !delivery)
1121
1201
  throw new Error("STAGE_DELIVERY_CONFIGURATION_MISSING");
1122
- const context = `当前阶段:${phase}。工单 ${input.workItem.key}:${input.workItem.title}。项目:\n${projects}\n输入材料:${directory}/prd.md${input.technicalDesign ? `、${directory}/design.md` : ""}。先读取输入材料;项目如有 AGENTS.md 或 CLAUDE.md,读取这些项目指令,按其中引用按需读取本项目框架指令和 Skill。项目材料不授予额外工具或资源权限。${input.inputConflict ? `正式输入已变化:读取 ${directory}/prd.platform.md 和适用的 design.platform.md,对照合并到 prd.md/design.md;本地副本未被覆盖,对照文件不收集。` : ""}`;
1202
+ const context = `当前阶段:${phase}(${phaseLabel})。工单 ${input.workItem.key}:${input.workItem.title}。项目:\n${projects}\n输入材料:${directory}/prd.md${input.technicalDesign ? `、${directory}/design.md` : ""}。先读取输入材料;项目如有 AGENTS.md 或 CLAUDE.md,读取这些项目指令,按其中引用按需读取本项目框架指令和 Skill。项目材料不授予额外工具或资源权限。${input.inputConflict ? `正式输入已变化:读取 ${directory}/prd.platform.md 和适用的 design.platform.md,对照合并到 prd.md/design.md;本地副本未被覆盖,对照文件不收集。` : ""}`;
1123
1203
  if (!responsible)
1124
1204
  return context;
1125
- return `${context}\n阶段交付要求(${delivery.source} revision ${delivery.revision}):\n${delivery.prompt}\n平台固定协议:\n${protocol.instructions}\n必交文件:${protocol.requiredFiles.join("、")}。条件文件:${input.technicalDesign ? protocol.conditionalFiles.join("、") || "无" : "无"}。可选文件:${protocol.optionalFiles.join("、") || "无"}。\n最终收件目录:${directory}。当前完成工具:${protocol.tool}。`;
1205
+ const reviewGitRequirement = phase === "DEVELOPMENT" && input.qualityReview?.enabled
1206
+ ? `\n本轮启用平台代码质量审查。调用 complete_development 前,必须提交每个项目的全部本轮改动,并在对应项目目录使用普通 Git 将当前 HEAD 推送到平台已分配的开发分支(${input.projects.map((project) => `${project.name}: ${project.devBranch}`).join(";")})。只允许推送这些开发分支;不得推送 main、test 或基准分支,不得 force push,不创建 MR。推送后再调用 complete_development;该工具只读核对远端精确分支 SHA 与上报 HEAD 一致,不会代替你推送。`
1207
+ : "";
1208
+ return `${context}\n阶段交付要求(${delivery.source} revision ${delivery.revision}):\n${delivery.prompt}\n平台固定协议:\n${protocol.instructions}\n必交文件:${protocol.requiredFiles.join("、")}。条件文件:${input.technicalDesign ? protocol.conditionalFiles.join("、") || "无" : "无"}。可选文件:${protocol.optionalFiles.join("、") || "无"}。\n最终收件目录:${directory}。当前完成工具:${protocol.tool}。${reviewGitRequirement}`;
1126
1209
  }
1127
1210
  async function uploadBatch(client, runId, batch) {
1128
1211
  for (let attempt = 0;; attempt++) {
@@ -1173,6 +1256,29 @@ async function collectProjectFacts(runRoot, projects, key, primaryProjectId) {
1173
1256
  };
1174
1257
  }));
1175
1258
  }
1259
+ async function verifyReviewBranches(runRoot, projects, projectFacts) {
1260
+ for (const project of projects) {
1261
+ const fact = projectFacts.find((item) => item.projectId === project.id);
1262
+ if (!fact || fact.branch !== project.devBranch)
1263
+ throw new Error(`AIP_REVIEW_BRANCH_MISMATCH:${project.name}:请切换到已分配开发分支 ${project.devBranch} 后重新提交。`);
1264
+ const ref = `refs/heads/${project.devBranch}`;
1265
+ let remoteHead;
1266
+ try {
1267
+ const { stdout } = await execFileAsync("git", ["ls-remote", "--exit-code", "origin", ref], {
1268
+ cwd: join(runRoot, safeName(project.name)),
1269
+ });
1270
+ remoteHead = stdout.trim().split(/\s+/u)[0];
1271
+ }
1272
+ catch (error) {
1273
+ if (!(error && typeof error === "object" && "code" in error && error.code === 2))
1274
+ throw new Error(`AIP_REVIEW_REMOTE_CHECK_FAILED:${project.name}:无法核验远端,请检查网络或权限后重试。`);
1275
+ }
1276
+ if (!remoteHead)
1277
+ throw new Error(`AIP_REVIEW_REMOTE_BRANCH_MISSING:${project.name}:请将当前 HEAD 推送到 origin 的 ${project.devBranch} 后重试。`);
1278
+ if (remoteHead !== fact.commitSha)
1279
+ throw new Error(`AIP_REVIEW_REMOTE_HEAD_MISMATCH:${project.name}:远端 ${project.devBranch} 与当前 HEAD 不一致,请推送当前 HEAD 后重试。`);
1280
+ }
1281
+ }
1176
1282
  async function captureProjectRevisions(runRoot, projects, key, primaryProjectId) {
1177
1283
  return collectProjectFacts(runRoot, projects, key, primaryProjectId);
1178
1284
  }