@bf-meishijian/aip 0.0.4 → 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 (60) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +14 -15
  4. package/dist/cli.js.map +1 -1
  5. package/dist/coding-run-manager.d.ts +8 -2
  6. package/dist/coding-run-manager.d.ts.map +1 -1
  7. package/dist/coding-run-manager.js +776 -544
  8. package/dist/coding-run-manager.js.map +1 -1
  9. package/dist/coding-subagent-child.d.ts +22 -0
  10. package/dist/coding-subagent-child.d.ts.map +1 -0
  11. package/dist/coding-subagent-child.js +90 -0
  12. package/dist/coding-subagent-child.js.map +1 -0
  13. package/dist/coding-subagents.d.ts +63 -0
  14. package/dist/coding-subagents.d.ts.map +1 -0
  15. package/dist/coding-subagents.js +537 -0
  16. package/dist/coding-subagents.js.map +1 -0
  17. package/dist/command-wakeup.d.ts +19 -0
  18. package/dist/command-wakeup.d.ts.map +1 -0
  19. package/dist/command-wakeup.js +109 -0
  20. package/dist/command-wakeup.js.map +1 -0
  21. package/dist/daemon.d.ts.map +1 -1
  22. package/dist/daemon.js +68 -43
  23. package/dist/daemon.js.map +1 -1
  24. package/dist/delivery-files.d.ts +12 -0
  25. package/dist/delivery-files.d.ts.map +1 -1
  26. package/dist/delivery-files.js +80 -0
  27. package/dist/delivery-files.js.map +1 -1
  28. package/dist/harness.d.ts +2 -0
  29. package/dist/harness.d.ts.map +1 -0
  30. package/dist/harness.js +1 -0
  31. package/dist/harness.js.map +1 -0
  32. package/dist/live-employee.d.ts +4 -2
  33. package/dist/live-employee.d.ts.map +1 -1
  34. package/dist/live-employee.js +49 -15
  35. package/dist/live-employee.js.map +1 -1
  36. package/dist/model-stores.d.ts +20 -0
  37. package/dist/model-stores.d.ts.map +1 -0
  38. package/dist/model-stores.js +77 -0
  39. package/dist/model-stores.js.map +1 -0
  40. package/dist/pi-message-queue.d.ts +15 -0
  41. package/dist/pi-message-queue.d.ts.map +1 -0
  42. package/dist/pi-message-queue.js +57 -0
  43. package/dist/pi-message-queue.js.map +1 -0
  44. package/dist/pi-model-service.d.ts.map +1 -1
  45. package/dist/pi-model-service.js +47 -45
  46. package/dist/pi-model-service.js.map +1 -1
  47. package/dist/platform-client.d.ts +16 -0
  48. package/dist/platform-client.d.ts.map +1 -1
  49. package/dist/platform-client.js +21 -0
  50. package/dist/platform-client.js.map +1 -1
  51. package/dist/review-report.d.ts +10 -0
  52. package/dist/review-report.d.ts.map +1 -0
  53. package/dist/review-report.js +59 -0
  54. package/dist/review-report.js.map +1 -0
  55. package/dist/stage-delivery.js +3 -2
  56. package/dist/state.d.ts +3 -2
  57. package/dist/state.d.ts.map +1 -1
  58. package/dist/state.js +5 -2
  59. package/dist/state.js.map +1 -1
  60. package/package.json +13 -7
@@ -2,18 +2,22 @@ import { execFile } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import { mkdir, mkdtemp, open, realpath, rm, stat } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
- import { basename, join, resolve } from "node:path";
5
+ import { basename, dirname, join, resolve } from "node:path";
6
6
  import { promisify } from "node:util";
7
- import { DELIVERY_FILES, STAGE_DELIVERY_PROTOCOLS, stageEmployeeIds } from "./stage-delivery.js";
8
7
  import { createAgentSession, DefaultResourceLoader, SessionManager, } from "@earendil-works/pi-coding-agent";
9
8
  import { Type } from "typebox";
10
- 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";
11
11
  import { liveEmployee } from "./live-employee.js";
12
12
  import { prepareMergeRequest, registerMergeRequest, } from "./merge-requests.js";
13
13
  import { paths } from "./paths.js";
14
+ import { PiMessageQueue } from "./pi-message-queue.js";
14
15
  import { PlatformClientError } from "./platform-client.js";
16
+ import { materializeReviewReport } from "./review-report.js";
15
17
  import { readJson, writeSecureJson } from "./secure-json.js";
18
+ import { DELIVERY_FILES, STAGE_DELIVERY_PROTOCOLS, stageEmployeeIds } from "./stage-delivery.js";
16
19
  const execFileAsync = promisify(execFile);
20
+ const EVENT_APPEND_MAX_ATTEMPTS = 8;
17
21
  export class CodingRunManager {
18
22
  pi;
19
23
  active = new Map();
@@ -26,6 +30,25 @@ export class CodingRunManager {
26
30
  async execute(client, host, command) {
27
31
  if (!command.run)
28
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
+ }
29
52
  const imageRun = command.run;
30
53
  let images;
31
54
  try {
@@ -40,6 +63,7 @@ export class CodingRunManager {
40
63
  if (active?.turnId === imageRun.turnId) {
41
64
  active.stopping = true;
42
65
  active.observed.stopping = true;
66
+ await active.subagents?.stop();
43
67
  await active.session.abort();
44
68
  await active.finished;
45
69
  if (this.active.get(imageRun.runId) === active)
@@ -51,7 +75,18 @@ export class CodingRunManager {
51
75
  const input = command.run.runInput;
52
76
  await this.start(client, host, command.run.runId, command.run.turnId, input, false, command.run.contentMarkdown, images);
53
77
  }
54
- 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") {
55
90
  let active = this.active.get(command.run.runId);
56
91
  if (!command.run.contentMarkdown && images.length === 0)
57
92
  throw new Error("CODING_RUN_MESSAGE_MISSING");
@@ -60,25 +95,63 @@ export class CodingRunManager {
60
95
  throw new Error("CODING_RUN_TURN_MISMATCH");
61
96
  active.stopping = true;
62
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();
63
105
  await active.session.abort();
64
106
  if (this.active.get(command.run.runId) === active)
65
107
  this.active.delete(command.run.runId);
66
108
  active = undefined;
67
109
  }
68
110
  if (active) {
69
- if (images.length > 0 && !active.session.model?.input.includes("image"))
111
+ if (images.length > 0 && !active.supportsImages)
70
112
  throw new Error("MODEL_IMAGES_UNSUPPORTED");
71
- await active.emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
72
- if (images.length > 0)
73
- await active.session.steer(command.run.contentMarkdown ?? "", images);
74
- else
75
- await active.session.steer(command.run.contentMarkdown ?? "");
76
- await active.emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
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
+ });
77
145
  }
78
146
  else {
79
147
  if (!command.run.runInput)
80
148
  throw new Error("CODING_RUN_INPUT_MISSING");
81
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
+ });
82
155
  }
83
156
  }
84
157
  else if (command.run.action === "STOP") {
@@ -88,33 +161,55 @@ export class CodingRunManager {
88
161
  throw new Error("CODING_RUN_TURN_MISMATCH");
89
162
  active.stopping = true;
90
163
  active.observed.stopping = true;
164
+ await active.subagents?.stop();
91
165
  await active.session.abort();
92
- for (const [activityKey, agentId] of active.observed.activeTasks) {
93
- await active.emit("SUBAGENT_STOPPED", {
94
- activityKey,
95
- agentId,
96
- summary: "随主执行停止",
97
- });
98
- }
99
- active.observed.activeTasks.clear();
100
- await active.emit("RUN_STOPPED", {});
166
+ await active.emit("AUTO_CONTINUATION_CHANGED", {
167
+ phase: active.input.phase,
168
+ autoContinueEnabled: false,
169
+ reason: "USER_STOP",
170
+ source: "HOST_HANDLED",
171
+ });
172
+ await active.emit.flush();
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" });
101
180
  }
102
181
  else {
103
182
  const stateFile = join(paths().runState, `${command.run.runId}.json`);
104
183
  const state = await readJson(stateFile);
105
- 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
+ });
106
189
  }
107
190
  }
108
191
  else if (command.run.action === "RESUME") {
109
192
  await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
110
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
+ }
111
206
  }
112
- async start(client, host, runId, turnId, input, resume, resumePrompt, images = []) {
207
+ async start(client, host, runId, turnId, input, resume, resumePrompt, images = [], continuation) {
113
208
  const active = this.active.get(runId);
114
209
  if (active) {
115
210
  if (active.turnId === turnId)
116
211
  return;
117
- if (!active.observed.terminalReached)
212
+ if (!active.observed.terminalReached && !active.stopping)
118
213
  throw new Error("CODING_RUN_TURN_MISMATCH");
119
214
  await active.finished;
120
215
  if (this.active.get(runId) === active)
@@ -127,13 +222,25 @@ export class CodingRunManager {
127
222
  const local = paths();
128
223
  const runRoot = join(local.runWorktrees, runId);
129
224
  await mkdir(runRoot, { recursive: true, mode: 0o700 });
130
- await mkdir(local.piSessions, { recursive: true, mode: 0o700 });
131
- await mkdir(local.piAgent, { recursive: true, mode: 0o700 });
132
225
  const stateFile = join(local.runState, `${runId}.json`);
133
226
  const storedState = await readJson(stateFile);
134
- const cwd = await prepareProjects(local.repositoryCache, runRoot, runId, input.projects, Boolean(storedState?.prepared));
227
+ if (storedState?.phaseSessions &&
228
+ Object.keys(storedState.phaseSessions).length &&
229
+ storedState.harness !== "pi")
230
+ throw new Error("PRIOR_HARNESS_SESSION_UNSUPPORTED:请新建执行,历史文件和工作目录已保留。");
135
231
  for (const project of input.projects)
136
- 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
+ }
137
244
  if (!storedState?.prepared || input.inputConflict)
138
245
  await initializeDeliveryFiles(runRoot, input, Boolean(storedState?.prepared));
139
246
  const deliveryInputHash = createHash("sha256")
@@ -143,7 +250,7 @@ export class CodingRunManager {
143
250
  input.technicalDesign?.content,
144
251
  ]))
145
252
  .digest("hex");
146
- const childSessions = storedState?.childSessions ?? {};
253
+ const delegations = storedState?.delegations ?? {};
147
254
  let pendingDelivery = storedState?.phase !== input.phase ||
148
255
  resumePrompt ||
149
256
  images.length > 0 ||
@@ -159,11 +266,10 @@ export class CodingRunManager {
159
266
  : undefined;
160
267
  const phaseSessions = storedState?.phaseSessions ?? {};
161
268
  const sessionPhase = input.executionPurpose === "RELEASE" ? "DEVELOPMENT" : input.phase;
162
- let sessionFileRef = phaseSessions[sessionPhase];
269
+ let sessionRef = phaseSessions[sessionPhase];
163
270
  let lastEventSequence = storedState?.lastEventSequence ?? 0;
164
271
  const observed = {
165
- pendingQuestion: storedState?.phase === input.phase ? (storedState.pendingQuestion ?? "") : "",
166
- memberResults: new Map(),
272
+ userIntervened: input.autoContinueEnabled === false,
167
273
  activeTasks: new Map(),
168
274
  stopping: false,
169
275
  terminalReached: false,
@@ -178,11 +284,12 @@ export class CodingRunManager {
178
284
  primaryProjectId: input.primaryProjectId,
179
285
  },
180
286
  prepared: true,
287
+ preparationBaselines,
181
288
  phaseSessions,
182
- sessionFile: sessionFileRef,
289
+ sessionRef,
290
+ harness: "pi",
183
291
  lastEventSequence,
184
- pendingQuestion: observed.pendingQuestion,
185
- childSessions,
292
+ delegations,
186
293
  pendingDelivery,
187
294
  deliveryInputHash,
188
295
  });
@@ -192,15 +299,15 @@ export class CodingRunManager {
192
299
  return persistState();
193
300
  }, redact);
194
301
  if (input.phase === "PREPARATION") {
302
+ if (!storedState?.prepared)
303
+ await publishReviewMaterials(runRoot, input);
195
304
  await persistState();
196
305
  await emit("PREPARATION_COMPLETED", {
197
306
  primaryProjectId: input.primaryProjectId,
198
307
  projects: await Promise.all(input.projects.map(async (p) => ({
199
308
  projectId: p.id,
200
309
  projectName: p.name,
201
- baselineCommit: (await execFileAsync("git", ["rev-parse", "HEAD"], {
202
- cwd: join(runRoot, safeName(p.name)),
203
- })).stdout.trim(),
310
+ baselineCommit: preparationBaselines[p.id] ?? p.baselineCommit,
204
311
  directory: join(runRoot, safeName(p.name)),
205
312
  branch: (await execFileAsync("git", ["branch", "--show-current"], {
206
313
  cwd: join(runRoot, safeName(p.name)),
@@ -209,12 +316,12 @@ export class CodingRunManager {
209
316
  });
210
317
  return;
211
318
  }
212
- const terminal = terminalExtension(client, host, runId, turnId, runRoot, input, observed, redact, async (batch) => {
319
+ const terminal = terminalTools(client, host, runId, turnId, runRoot, input, observed, async (batch) => {
213
320
  pendingDelivery = batch;
214
321
  await persistState();
215
- }, () => pendingDelivery, (question) => emit("QUESTION_UPDATED", { question: redact(question) }), () => emit.flush());
322
+ }, () => pendingDelivery, () => emit.flush());
216
323
  if (!this.pi.resolveRuntimeModel)
217
- throw new Error("PI_RUNTIME_UNAVAILABLE");
324
+ throw new Error("MODEL_RUNTIME_UNAVAILABLE");
218
325
  const resolved = await this.pi.resolveRuntimeModel(input.providerId, input.modelId, {
219
326
  runId,
220
327
  turnId,
@@ -230,215 +337,143 @@ export class CodingRunManager {
230
337
  member: "supervisor",
231
338
  };
232
339
  const availableAgents = stageEmployeeIds(input);
233
- const live = liveEmployee(client, runId, binding, await client.employeeConfig(runId, { ...binding, purpose: "LOAD" }), runRoot, availableAgents.length > 0, input.executionPurpose === "RELEASE", input.phase, () => observed.terminalReached);
234
- const childInstances = new Map();
235
- const childUpdates = new Map();
236
- const childTasks = new Map();
237
- const team = (api) => {
238
- api.registerTool({
239
- name: "subagent",
240
- label: "派发研发团队",
241
- description: `向实际绑定员工分派子任务,返回真实子会话结果。可用员工:${availableAgents.map((id) => `${id}:${input.teamEmployees?.[id]?.name ?? id};${input.teamEmployees?.[id]?.description ?? ""}`).join("\n")}。`,
242
- parameters: Type.Object({
243
- agent: Type.Union(availableAgents.map((name) => Type.Literal(name))),
244
- task: Type.String({ minLength: 1 }),
245
- }),
246
- async execute(callId, params, signal, onUpdate) {
247
- const member = params.agent;
248
- if (!availableAgents.includes(member))
249
- throw new Error("DEVELOPMENT_TEAM_MEMBER_INVALID");
250
- childTasks.set(member, callId);
251
- const childBinding = { ...binding, member };
252
- let child = childInstances.get(member);
253
- observed.memberResults.delete(params.agent);
254
- childUpdates.set(member, (text) => onUpdate?.({ content: [{ type: "text", text }], details: { agent: params.agent } }));
255
- if (!child) {
256
- const childLive = liveEmployee(client, runId, childBinding, await client.employeeConfig(runId, { ...childBinding, purpose: "LOAD" }), runRoot, true, false, input.phase);
257
- const resultExtension = (pi) => pi.registerTool({
258
- name: "submit_member_result",
259
- label: "提交成员结果",
260
- description: "如实记录本次子任务的结果,未解决的问题必须 FAIL。仅向主管汇报,不确认最终交付。",
261
- parameters: Type.Object({
262
- status: Type.Union([Type.Literal("PASS"), Type.Literal("FAIL")]),
263
- summary: Type.String({ minLength: 1 }),
264
- }),
265
- async execute(_id, result) {
266
- const revisions = (await captureProjectRevisions(runRoot, input.projects, input.workItem.key, input.primaryProjectId)).filter((r) => Boolean(r));
267
- observed.memberResults.set(params.agent, {
268
- status: result.status,
269
- summary: result.summary,
270
- revisions,
271
- });
272
- return toolResult("结果已记录,向主管返回结论。");
273
- },
274
- });
275
- const childLoader = new DefaultResourceLoader({
276
- cwd,
277
- agentDir: local.piAgent,
278
- noExtensions: true,
279
- noSkills: true,
280
- noPromptTemplates: true,
281
- noThemes: true,
282
- noContextFiles: true,
283
- extensionFactories: [childLive.extension, resultExtension],
284
- systemPrompt: "当前为团队子成员会话,员工配置由平台下发。",
285
- });
286
- await childLoader.reload();
287
- const file = childSessions[`${input.phase}:${member}`];
288
- const childManager = file
289
- ? SessionManager.open(join(local.piSessions, file), local.piSessions, cwd)
290
- : SessionManager.create(cwd, local.piSessions);
291
- const created = await createAgentSession({
292
- cwd,
293
- agentDir: local.piAgent,
294
- modelRuntime: resolved.runtime,
295
- model: resolved.model,
296
- thinkingLevel: input.thinkingLevel,
297
- resourceLoader: childLoader,
298
- sessionManager: childManager,
299
- });
300
- child = created.session;
301
- childLive.attach(child);
302
- childInstances.set(member, child);
303
- const childFile = childManager.getSessionFile();
304
- if (!childFile)
305
- throw new Error("CHILD_SESSION_FILE_MISSING");
306
- childSessions[`${input.phase}:${member}`] = basename(childFile);
307
- await persistState();
308
- subscribeSession(child, async (kind, payload) => {
309
- if (kind === "MESSAGE_STARTED")
310
- return;
311
- if (kind === "TEXT_DELTA") {
312
- childUpdates.get(member)?.(String(payload.text ?? ""));
313
- return;
314
- }
315
- await emit(kind, {
316
- ...payload,
317
- agentId: params.agent,
318
- phase: input.phase,
319
- parentActivityKey: childTasks.get(member),
320
- sessionId: childSessions[`${input.phase}:${member}`],
321
- });
322
- }, observed, redact);
323
- }
324
- await emit("SUBAGENT_UPDATED", {
325
- phase: input.phase,
326
- task: params.task,
327
- activityKey: callId,
328
- agentId: params.agent,
329
- sessionFileRef: childSessions[`${input.phase}:${member}`],
330
- summary: "独立子会话已就绪",
331
- });
332
- const abort = () => {
333
- void child?.abort();
334
- };
335
- signal?.addEventListener("abort", abort, { once: true });
336
- try {
337
- await child.prompt(`${initialPrompt(input, runRoot, false)}\n\n本次主管分派给 ${params.agent} 的任务:${params.task}\n只完成自身职责,最后调用 submit_member_result;主管负责其他成员协调和最终交付。`);
338
- await child.waitForIdle();
339
- if (signal?.aborted || observed.stopping)
340
- throw new Error("SUBAGENT_STOPPED");
341
- const result = observed.memberResults.get(params.agent);
342
- if (!result)
343
- throw new Error("SUBAGENT_RESULT_MISSING");
344
- return {
345
- content: [{ type: "text", text: JSON.stringify(result) }],
346
- details: {
347
- agent: params.agent,
348
- status: result.status,
349
- sessionFileRef: childSessions[`${input.phase}:${member}`],
350
- },
351
- };
352
- }
353
- finally {
354
- childUpdates.delete(member);
355
- signal?.removeEventListener("abort", abort);
356
- }
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);
357
373
  },
358
374
  });
359
- };
360
- const loader = new DefaultResourceLoader({
375
+ const opened = await openPiSession({
361
376
  cwd,
377
+ sessionDir: local.piSessions,
362
378
  agentDir: local.piAgent,
363
- extensionFactories: [...(availableAgents.length ? [team] : []), terminal, live.extension],
364
- noSkills: true,
365
- noExtensions: true,
366
- noPromptTemplates: true,
367
- noThemes: true,
368
- noContextFiles: true,
379
+ existingId: sessionRef,
380
+ modelRuntime: resolved.runtime,
381
+ model: resolved.model,
382
+ thinkingLevel: input.thinkingLevel,
383
+ extensions: [...(delegation ? [delegation.extension] : []), terminal, live.extension],
369
384
  systemPrompt: input.team
370
385
  ? "当前为团队主负责人会话,员工配置由平台下发。"
371
386
  : "当前为工单主会话,员工配置由平台下发。",
387
+ runId,
372
388
  });
373
- await loader.reload();
374
- const existingSession = phaseSessions[sessionPhase]
375
- ? join(local.piSessions, phaseSessions[sessionPhase])
376
- : null;
377
- let sessionManager;
378
- if (existingSession) {
379
- sessionManager = SessionManager.open(existingSession, local.piSessions, cwd);
380
- }
381
- else {
382
- sessionManager = SessionManager.create(cwd, local.piSessions);
383
- const allocatedSessionFile = sessionManager.getSessionFile();
384
- if (!allocatedSessionFile)
385
- throw new Error("PI_SESSION_FILE_MISSING");
386
- const placeholder = await open(allocatedSessionFile, "wx", 0o600);
387
- await placeholder.close();
388
- sessionManager.setSessionFile(allocatedSessionFile);
389
- sessionManager.appendCustomEntry("aip-run-binding", {
390
- runId,
389
+ live.attach(opened.session);
390
+ const session = opened.session;
391
+ sessionRef = opened.sessionId;
392
+ observed.messageQueue = new PiMessageQueue(session);
393
+ subscribeSession(session, (kind, payload) => {
394
+ const activityKey = String(payload.activityKey ?? "");
395
+ if (kind === "SUBAGENT_STARTED")
396
+ observed.activeTasks.set(activityKey, String(payload.agentId));
397
+ if (["SUBAGENT_COMPLETED", "SUBAGENT_FAILED", "SUBAGENT_STOPPED"].includes(kind))
398
+ observed.activeTasks.delete(activityKey);
399
+ return emit(kind, {
400
+ ...payload,
401
+ phase: input.phase,
402
+ agentId: payload.agentId ?? "supervisor",
403
+ sessionId: sessionRef,
391
404
  });
392
- }
393
- const sessionFile = sessionManager.getSessionFile();
394
- if (!sessionFile)
395
- throw new Error("PI_SESSION_FILE_MISSING");
396
- if (!(await exists(sessionFile)))
397
- throw new Error("PI_SESSION_FILE_NOT_PERSISTED");
398
- const { session } = await createAgentSession({
399
- cwd,
400
- agentDir: local.piAgent,
401
- modelRuntime: resolved.runtime,
402
- model: resolved.model,
403
- thinkingLevel: input.thinkingLevel,
404
- resourceLoader: loader,
405
- sessionManager,
406
- });
407
- live.attach(session);
408
- const activeRun = { session, input, turnId, emit, stopping: false, observed };
405
+ }, observed, redact);
406
+ const activeRun = {
407
+ session,
408
+ supportsImages: resolved.model.input?.includes("image") === true,
409
+ input,
410
+ turnId,
411
+ emit,
412
+ stopping: false,
413
+ observed,
414
+ ...(delegation ? { subagents: delegation } : {}),
415
+ };
409
416
  this.active.set(runId, activeRun);
410
- subscribeSession(session, (kind, payload) => emit(kind, {
411
- ...payload,
412
- phase: input.phase,
413
- agentId: payload.agentId ?? "supervisor",
414
- sessionId: sessionFileRef,
415
- }), observed, redact);
416
- sessionFileRef = basename(sessionFile);
417
- phaseSessions[sessionPhase] = sessionFileRef;
417
+ phaseSessions[sessionPhase] = sessionRef;
418
418
  await persistState();
419
- await emit("RUN_STARTED", { sessionFileRef, phase: input.phase });
420
- if (resumePrompt || images.length > 0) {
419
+ await emit("RUN_STARTED", { sessionRef, phase: input.phase });
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)) {
421
430
  await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
422
431
  await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
423
432
  }
424
- const prompt = `${initialPrompt(input, runRoot)}${resumePrompt ? `\n\n${resumePrompt}` : resume ? "\n\n继续当前会话未完成的工作,先读取最新文件和交接结果。" : ""}`;
425
- 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)
426
442
  .catch(async (error) => {
427
443
  if (activeRun.stopping)
428
444
  return;
445
+ await emit("EXECUTION_DECIDED", {
446
+ phase: input.phase,
447
+ decision: "SKIP",
448
+ reason: "MODEL_ERROR",
449
+ stopReason: "error",
450
+ });
429
451
  await emit("RUN_FAILED", {
430
- 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)),
431
457
  });
432
458
  })
433
459
  .finally(async () => {
434
460
  await emit.flush();
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
+ }
435
470
  if (this.active.get(runId) === activeRun)
436
471
  this.active.delete(runId);
437
472
  });
438
473
  }
439
474
  async synthetic(client, host, runId, turnId, input) {
440
475
  const emit = eventEmitter(client, host, runId, turnId);
441
- await emit("RUN_STARTED", { sessionFileRef: `${runId}.jsonl` });
476
+ await emit("RUN_STARTED", { sessionRef: `synthetic:${runId}` });
442
477
  const member = stageEmployeeIds(input)[0];
443
478
  if (member)
444
479
  await emit("SUBAGENT_STARTED", {
@@ -484,29 +519,46 @@ function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistS
484
519
  let sequence = initialSequence;
485
520
  let queue = Promise.resolve();
486
521
  const emit = (kind, payload) => {
487
- sequence += 1;
488
- const event = {
489
- eventId: crypto.randomUUID(),
490
- sequence,
491
- turnId,
492
- kind,
493
- payload: kind === "PREPARATION_COMPLETED" ? payload : redactPayload(payload, redact),
494
- createdAt: new Date().toISOString(),
495
- };
496
522
  queue = queue
497
- .then(() => client.appendCodingEvents(runId, {
498
- hostId: host.hostId,
499
- generation: host.generation,
500
- events: [event],
501
- }))
502
- .then(() => persistSequence?.(event.sequence))
503
- .then(() => undefined)
504
- .catch((error) => {
505
- if (error instanceof PlatformClientError &&
506
- error.status === 409 &&
507
- error.code === "CODING_EVENT_TURN_STALE")
508
- return;
509
- throw error;
523
+ .catch(() => { })
524
+ .then(async () => {
525
+ const event = {
526
+ eventId: crypto.randomUUID(),
527
+ sequence: sequence + 1,
528
+ turnId,
529
+ kind,
530
+ payload: kind === "PREPARATION_COMPLETED" ? payload : redactPayload(payload, redact),
531
+ createdAt: new Date().toISOString(),
532
+ };
533
+ for (let attempt = 1; attempt <= EVENT_APPEND_MAX_ATTEMPTS; attempt++) {
534
+ try {
535
+ await client.appendCodingEvents(runId, {
536
+ hostId: host.hostId,
537
+ generation: host.generation,
538
+ events: [event],
539
+ });
540
+ sequence = event.sequence;
541
+ await persistSequence?.(event.sequence);
542
+ return;
543
+ }
544
+ catch (error) {
545
+ if (error instanceof PlatformClientError &&
546
+ error.status === 409 &&
547
+ error.code === "CODING_EVENT_TURN_STALE") {
548
+ if (["EXECUTION_STARTED", "EXECUTION_PAUSED", "CONVERSATION_COMPLETED"].includes(kind))
549
+ throw error;
550
+ return;
551
+ }
552
+ process.stderr.write(`[local-coding-host] 事件上报失败:${redact(error instanceof Error ? error.message : String(error))}\n`);
553
+ if (attempt === EVENT_APPEND_MAX_ATTEMPTS) {
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;
557
+ return;
558
+ }
559
+ await new Promise((resolve) => setTimeout(resolve, 0));
560
+ }
561
+ }
510
562
  });
511
563
  return queue;
512
564
  };
@@ -524,227 +576,56 @@ function redactPayloadValue(value, redact) {
524
576
  return redactPayload(value, redact);
525
577
  return value;
526
578
  }
527
- async function driveToTerminal(session, initial, observed, client, host, runId, turnId, emit, release, phase, images = []) {
528
- let prompt = initial;
529
- for (let attempt = 0; attempt <= 2; attempt += 1) {
530
- if (!session.model?.input.includes("image") &&
531
- session.messages?.some((message) => "content" in message &&
532
- Array.isArray(message.content) &&
533
- message.content.some((block) => block.type === "image")))
534
- throw new Error("MODEL_IMAGES_UNSUPPORTED");
535
- if (attempt === 0 && images.length > 0)
536
- await session.prompt(prompt, { images });
537
- else
538
- await session.prompt(prompt);
539
- await session.waitForIdle();
540
- if (observed.stopping || observed.terminalReached || observed.pendingQuestion)
541
- return;
542
- if (attempt < 2) {
543
- prompt = release
544
- ? `请调用 submit_pull_requests 完成提 PR;最近错误:${observed.lastDeliveryError ?? "尚未提交"}。`
545
- : `你尚未成功交付。自主补齐必需产物后调用 ${phase === "DEVELOPMENT" ? "complete_development" : "submit_delivery"}。最近提交结果:${observed.lastDeliveryError ?? "尚未提交"}。缺文件就在当前会话补齐,传输故障不要重写报告。无法继续时说明真实原因,需要沟通可记录待回应问题。`;
546
- continue;
547
- }
548
- await client.reportCodingTerminal(runId, {
549
- hostId: host.hostId,
550
- generation: host.generation,
551
- turnId,
552
- kind: "FAILED",
553
- summary: observed.lastDeliveryError ?? "Agent 多次结束但没有形成明确终态",
554
- knownIssues: "",
555
- failureCode: observed.lastDeliveryError?.split("\n")[0]?.slice(0, 200) ?? "PI_TERMINAL_TOOL_NOT_CALLED",
556
- projectFacts: [],
557
- testFacts: [],
558
- });
559
- observed.terminalReached = true;
560
- await emit("RUN_FAILED", { code: "PI_TERMINAL_TOOL_NOT_CALLED" });
579
+ async function openPiSession(options) {
580
+ await mkdir(options.sessionDir, { recursive: true, mode: 0o700 });
581
+ await mkdir(options.agentDir, { recursive: true, mode: 0o700 });
582
+ let sessionManager;
583
+ if (options.existingId) {
584
+ const listed = await SessionManager.list(options.cwd, options.sessionDir);
585
+ const match = listed.find((item) => item.id === options.existingId);
586
+ if (!match)
587
+ throw new Error("PI_SESSION_NOT_FOUND:请新建执行,历史文件已保留。");
588
+ sessionManager = SessionManager.open(match.path, options.sessionDir, options.cwd);
561
589
  }
562
- }
563
- function terminalExtension(client, host, runId, turnId, runRoot, input, observed, redact, persistBatch, pendingBatch, emitQuestion, flushEvents) {
564
- return (pi) => {
565
- pi.registerTool({
566
- name: input.executionPurpose === "RELEASE"
567
- ? "submit_pull_requests"
568
- : input.phase === "DEVELOPMENT"
569
- ? "complete_development"
570
- : "submit_delivery",
571
- label: "提交交付",
572
- description: input.executionPurpose === "RELEASE"
573
- ? "核实上线清单并提交分支后,使用 Runtime GitLab 身份创建或复用 MR 并登记工单。"
574
- : "读取并校验本阶段最终交付文件,上传成功后完成当前阶段。",
575
- parameters: input.executionPurpose === "RELEASE"
576
- ? Type.Object({
577
- checklistSummary: Type.String({
578
- description: "核实依据、无需修改或修改摘要;缺口未解决时不要提交",
579
- }),
580
- projects: Type.Array(Type.Object({
581
- project: Type.String({
582
- description: `项目名称,只能选择:${input.projects.map((p) => p.name).join("、")}`,
583
- }),
584
- title: Type.String({ description: "包含工单编号及本项目变更摘要的 MR 标题" }),
585
- description: Type.String({
586
- description: "任务目的、本项目改动、测试与验收结论、发布注意、依赖、风险回滚及清单和报告链接",
587
- }),
588
- })),
589
- })
590
- : Type.Object({}),
591
- async execute(_toolCallId, rawParams) {
592
- try {
593
- if (input.executionPurpose === "RELEASE") {
594
- const params = rawParams;
595
- if (!params?.checklistSummary?.trim())
596
- throw new Error("AIP_RELEASE_CHECKLIST_SUMMARY_REQUIRED");
597
- const files = await collectDeliveryFiles(runRoot, input, ["RELEASE_CHECKLIST"]);
598
- const checklist = {
599
- status: "VERIFIED",
600
- summary: params.checklistSummary,
601
- markdown: files[0].content,
602
- };
603
- await client.submitPullRequests(runId, {
604
- hostId: host.hostId,
605
- generation: host.generation,
606
- turnId,
607
- workItemId: input.workItem.id,
608
- projects: [],
609
- checklist,
610
- });
611
- const context = {
612
- runId,
613
- turnId,
614
- workItem: input.workItem,
615
- projects: input.projects,
616
- primaryProjectId: input.primaryProjectId,
617
- };
618
- for (const project of input.projects) {
619
- const explanation = params.projects?.find((item) => item.project === project.name);
620
- if (!explanation)
621
- throw new Error(`AIP_MR_DESCRIPTION_REQUIRED:${project.name}`);
622
- const pendingFile = join(runRoot, `mr-pending-${safeName(project.id)}.json`);
623
- const result = await prepareMergeRequest({
624
- project,
625
- cwd: join(runRoot, safeName(project.name)),
626
- title: explanation.title,
627
- description: explanation.description,
628
- pendingFile,
629
- });
630
- await registerMergeRequest(client, context, host, result, pendingFile);
631
- }
632
- await client.submitPullRequests(runId, {
633
- hostId: host.hostId,
634
- generation: host.generation,
635
- turnId,
636
- workItemId: input.workItem.id,
637
- projects: [],
638
- completeRelease: true,
639
- });
640
- observed.terminalReached = true;
641
- return toolResult("上线清单已核实,MR 已提交并登记,等待合并;平台负责查询最新状态。");
642
- }
643
- const existing = pendingBatch();
644
- if (existing && existing.turnId === turnId) {
645
- await flushEvents();
646
- await uploadBatch(client, runId, existing);
647
- await persistBatch(undefined);
648
- observed.terminalReached = true;
649
- return toolResult("交付已提交,等待经办人确认。");
650
- }
651
- const files = await collectDeliveryFiles(runRoot, input, input.phase === "DEVELOPMENT"
652
- ? Object.keys(DELIVERY_FILES).filter((slot) => STAGE_DELIVERY_PROTOCOLS.DEVELOPMENT.requiredFiles.includes(DELIVERY_FILES[slot]))
653
- : undefined);
654
- const projectFacts = await collectProjectFacts(runRoot, input.projects, input.workItem.key, input.primaryProjectId);
655
- const batch = {
656
- hostId: host.hostId,
657
- generation: host.generation,
658
- turnId,
659
- kind: input.phase === "DEVELOPMENT" ? "DEVELOPMENT_COMPLETED" : "DELIVERY",
660
- submissionId: crypto.randomUUID(),
661
- files,
662
- summary: files.find((file) => file.slot === "SUMMARY").content,
663
- knownIssues: "",
664
- projectFacts,
665
- testFacts: [],
666
- };
667
- await persistBatch(batch);
668
- await flushEvents();
669
- await uploadBatch(client, runId, batch);
670
- await persistBatch(undefined);
671
- observed.terminalReached = true;
672
- return toolResult("交付已提交,等待经办人确认。");
673
- }
674
- catch (error) {
675
- observed.lastDeliveryError =
676
- error instanceof Error ? error.message : "AIP_DELIVERY_FAILED";
677
- if (error instanceof PlatformClientError && error.status < 500 && error.status !== 429)
678
- await persistBatch(undefined);
679
- throw error;
680
- }
681
- },
682
- });
683
- pi.registerTool({
684
- name: "update_question",
685
- label: "更新待回应问题",
686
- description: "仅记录当前执行必须由人决定的业务问题,不把一般建议变为审批;真正解决后传空字符串清除。普通聊天继续使用 steer,不停止 Run。",
687
- parameters: Type.Object({ question: Type.String() }),
688
- async execute(_id, params) {
689
- // The caller binding supplies technical identity; the model only owns the question text.
690
- observed.pendingQuestion = params.question.trim();
691
- await emitQuestion(params.question);
692
- return toolResult(params.question);
693
- },
694
- });
695
- if (input.executionPurpose === "RELEASE")
696
- pi.registerTool({
697
- name: "request_code_changes",
698
- label: "返回开发修改",
699
- description: "用户明确要求修改应用代码时,将工单返回开发,再测试和人工确认;仅讨论或文档修改不要调用。",
700
- parameters: Type.Object({
701
- reason: Type.String({ description: "用户要求修改的内容及原因" }),
702
- }),
703
- async execute(_id, params) {
704
- await client.request(`/api/v1/coding-runs/${runId}/return-to-development`, {
705
- method: "POST",
706
- body: JSON.stringify({
707
- hostId: host.hostId,
708
- generation: host.generation,
709
- turnId,
710
- reason: params.reason,
711
- }),
712
- });
713
- observed.terminalReached = true;
714
- return toolResult("已返回开发,等待下一执行接手续改,完成后进入测试与人工确认。");
715
- },
716
- });
717
- pi.registerTool({
718
- name: "report_blocker",
719
- label: "报告阻塞",
720
- description: "仅在认证、权限、仓库、外部环境或输入冲突真实阻塞时报告。",
721
- parameters: Type.Object({
722
- category: Type.Union([
723
- Type.Literal("AUTHENTICATION"),
724
- Type.Literal("PERMISSION"),
725
- Type.Literal("REPOSITORY"),
726
- Type.Literal("EXTERNAL_ENVIRONMENT"),
727
- Type.Literal("INPUT_CONFLICT"),
728
- ]),
729
- summary: Type.String(),
730
- }),
731
- async execute(_id, params) {
732
- await client.reportCodingTerminal(runId, {
733
- hostId: host.hostId,
734
- generation: host.generation,
735
- turnId,
736
- kind: "BLOCKED",
737
- summary: redact(params.summary),
738
- knownIssues: "",
739
- blockerCategory: params.category,
740
- projectFacts: [],
741
- testFacts: [],
742
- });
743
- observed.terminalReached = true;
744
- return toolResult("阻塞已报告。");
745
- },
746
- });
747
- };
590
+ else {
591
+ sessionManager = SessionManager.create(options.cwd, options.sessionDir);
592
+ const allocated = sessionManager.getSessionFile();
593
+ if (!allocated)
594
+ throw new Error("PI_SESSION_FILE_MISSING");
595
+ await mkdir(dirname(allocated), { recursive: true, mode: 0o700 });
596
+ const placeholder = await open(allocated, "wx", 0o600);
597
+ await placeholder.close();
598
+ sessionManager.setSessionFile(allocated);
599
+ sessionManager.appendCustomEntry("aip-run-binding", { runId: options.runId });
600
+ }
601
+ const sessionId = sessionManager.getSessionId();
602
+ const sessionFile = sessionManager.getSessionFile();
603
+ if (!sessionFile || !(await exists(sessionFile)))
604
+ throw new Error("PI_SESSION_FILE_NOT_PERSISTED");
605
+ const loader = new DefaultResourceLoader({
606
+ cwd: options.cwd,
607
+ agentDir: options.agentDir,
608
+ extensionFactories: options.extensions,
609
+ noSkills: true,
610
+ noExtensions: true,
611
+ noPromptTemplates: true,
612
+ noThemes: true,
613
+ noContextFiles: true,
614
+ systemPrompt: options.systemPrompt,
615
+ });
616
+ await loader.reload();
617
+ const { session } = await createAgentSession({
618
+ cwd: options.cwd,
619
+ agentDir: options.agentDir,
620
+ modelRuntime: options.modelRuntime,
621
+ model: options.model,
622
+ thinkingLevel: options.thinkingLevel,
623
+ resourceLoader: loader,
624
+ sessionManager,
625
+ });
626
+ await session.bindExtensions({ mode: "print" });
627
+ session.setAutoCompactionEnabled(true);
628
+ return { session, sessionId };
748
629
  }
749
630
  function subscribeSession(session, emit, observed, redact) {
750
631
  let messageId = crypto.randomUUID();
@@ -773,10 +654,31 @@ function subscribeSession(session, emit, observed, redact) {
773
654
  void emit(update.kind, update.payload);
774
655
  };
775
656
  session.subscribe((event) => {
776
- 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") {
777
666
  messageId = crypto.randomUUID();
778
667
  void emit("MESSAGE_STARTED", { messageId });
779
668
  }
669
+ else if (event.type === "message_start" && event.message.role === "user") {
670
+ const id = observed.messageQueue?.consumed(event.message);
671
+ if (id)
672
+ void emit("MESSAGE_QUEUE_UPDATED", { messageId: id, deliveryStatus: "READ" });
673
+ }
674
+ else if (event.type === "agent_end") {
675
+ observed.nativeWillRetry = event.willRetry === true;
676
+ }
677
+ else if (event.type === "auto_retry_end") {
678
+ observed.nativeWillRetry = false;
679
+ if (event.success === false && event.finalError)
680
+ observed.lastModelError = event.finalError;
681
+ }
780
682
  else if (event.type === "message_update" &&
781
683
  event.assistantMessageEvent.type === "text_delta" &&
782
684
  event.assistantMessageEvent.delta) {
@@ -788,78 +690,385 @@ function subscribeSession(session, emit, observed, redact) {
788
690
  }
789
691
  else if (event.type === "tool_execution_start") {
790
692
  flushText();
791
- const subagent = event.toolName === "subagent";
792
- if (subagent)
793
- observed.activeTasks.set(event.toolCallId, String(event.args?.agent));
794
- void emit(subagent ? "SUBAGENT_STARTED" : "TOOL_STARTED", subagent
795
- ? {
796
- activityKey: event.toolCallId,
797
- agentId: String(event.args?.agent ?? observed.activeTasks.get(event.toolCallId) ?? ""),
798
- summary: String(event.args?.task ?? "团队子任务开始"),
799
- task: String(event.args?.task ?? ""),
800
- }
801
- : {
802
- activityKey: event.toolCallId,
803
- toolName: event.toolName,
804
- summary: `执行 ${event.toolName}`,
805
- });
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
+ });
806
700
  }
807
701
  else if (event.type === "tool_execution_update") {
808
- pendingToolUpdates.set(event.toolCallId, event.toolName === "subagent"
809
- ? {
810
- kind: "SUBAGENT_UPDATED",
811
- payload: {
812
- activityKey: event.toolCallId,
813
- agentId: String(event.args?.agent ?? observed.activeTasks.get(event.toolCallId) ?? ""),
814
- currentAction: redact(textFromResult(event.partialResult)),
815
- },
816
- }
817
- : {
818
- kind: "TOOL_UPDATED",
819
- payload: {
820
- activityKey: event.toolCallId,
821
- toolName: event.toolName,
822
- currentAction: redact(textFromResult(event.partialResult)),
823
- },
824
- });
702
+ if (event.toolName === "subagent")
703
+ return;
704
+ pendingToolUpdates.set(event.toolCallId, {
705
+ kind: "TOOL_UPDATED",
706
+ payload: {
707
+ activityKey: event.toolCallId,
708
+ toolName: event.toolName,
709
+ currentAction: redact(textFromResult(event.partialResult)),
710
+ },
711
+ });
825
712
  if (!toolUpdateTimers.has(event.toolCallId))
826
713
  toolUpdateTimers.set(event.toolCallId, setTimeout(() => flushToolUpdate(event.toolCallId), 200));
827
714
  }
828
715
  else if (event.type === "tool_execution_end") {
829
- flushToolUpdate(event.toolCallId);
830
- const member = observed.activeTasks.get(event.toolCallId);
831
716
  if (event.toolName === "subagent")
832
- observed.activeTasks.delete(event.toolCallId);
833
- void emit(event.toolName === "subagent"
834
- ? observed.stopping
835
- ? "SUBAGENT_STOPPED"
836
- : event.isError
837
- ? "SUBAGENT_FAILED"
838
- : "SUBAGENT_COMPLETED"
839
- : "TOOL_COMPLETED", event.toolName === "subagent"
840
- ? {
841
- activityKey: event.toolCallId,
842
- agentId: String(event.result?.details?.agent ??
843
- member ??
844
- ""),
845
- summary: redact(textFromResult(event.result)),
846
- }
847
- : {
848
- activityKey: event.toolCallId,
849
- toolName: event.toolName,
850
- isError: event.isError === true,
851
- summary: redact(textFromResult(event.result)),
852
- });
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
+ });
725
+ }
726
+ else if (event.type === "bash_execution_update" && event.delta) {
727
+ const activityKey = event.id ?? "bash";
728
+ pendingToolUpdates.set(activityKey, {
729
+ kind: "TOOL_UPDATED",
730
+ payload: {
731
+ activityKey,
732
+ toolName: "bash",
733
+ currentAction: redact(event.delta),
734
+ },
735
+ });
736
+ if (!toolUpdateTimers.has(activityKey))
737
+ toolUpdateTimers.set(activityKey, setTimeout(() => flushToolUpdate(activityKey), 200));
738
+ }
739
+ else if (event.type === "compaction_start") {
740
+ flushText();
741
+ void emit("TOOL_STARTED", {
742
+ activityKey: `compaction:${event.reason}`,
743
+ toolName: "compaction",
744
+ summary: `上下文压缩(${event.reason})`,
745
+ });
746
+ }
747
+ else if (event.type === "compaction_end") {
748
+ void emit("TOOL_COMPLETED", {
749
+ activityKey: `compaction:${event.reason}`,
750
+ toolName: "compaction",
751
+ isError: event.aborted === true || Boolean(event.errorMessage),
752
+ summary: redact(event.result?.summary ??
753
+ event.errorMessage ??
754
+ (event.aborted ? "压缩已取消" : "压缩完成")),
755
+ });
853
756
  }
854
757
  else if (event.type === "message_end" && event.message.role === "assistant") {
855
758
  flushText();
759
+ if ("stopReason" in event.message && event.message.stopReason)
760
+ observed.lastModelStopReason = String(event.message.stopReason);
761
+ if ("errorMessage" in event.message && event.message.errorMessage)
762
+ observed.lastModelError = String(event.message.errorMessage);
856
763
  const content = textFromMessage(event.message.content);
857
- if (content)
858
- void emit("MESSAGE_COMPLETED", { contentMarkdown: redact(content), messageId });
764
+ const usage = event.message.usage;
765
+ const payload = {
766
+ contentMarkdown: redact(content),
767
+ messageId,
768
+ };
769
+ if (usage) {
770
+ payload.usage = {
771
+ input: usage.input,
772
+ output: usage.output,
773
+ cacheRead: usage.cacheRead,
774
+ cacheWrite: usage.cacheWrite,
775
+ totalTokens: usage.totalTokens,
776
+ };
777
+ }
778
+ if (content.trim())
779
+ void emit("MESSAGE_COMPLETED", payload);
859
780
  }
860
781
  });
861
782
  }
862
- export async function prepareProjects(cacheRoot, runRoot, runId, projects, resume) {
783
+ function textFromMessage(content) {
784
+ if (typeof content === "string")
785
+ return content;
786
+ if (!Array.isArray(content))
787
+ return "";
788
+ return content
789
+ .map((part) => part && typeof part === "object" && "text" in part ? String(part.text ?? "") : "")
790
+ .join("");
791
+ }
792
+ function textFromResult(value) {
793
+ if (!value || typeof value !== "object")
794
+ return "";
795
+ return textFromMessage(value.content);
796
+ }
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)
849
+ return;
850
+ await client.reportCodingTerminal(runId, {
851
+ ...host,
852
+ turnId,
853
+ kind: "FAILED",
854
+ summary: observed.lastModelError ?? code,
855
+ knownIssues: "",
856
+ failureCode: code,
857
+ projectFacts: [],
858
+ testFacts: [],
859
+ });
860
+ observed.terminalReached = true;
861
+ await emit("RUN_FAILED", { phase, code, message: observed.lastModelError ?? code });
862
+ return;
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;
908
+ }
909
+ function terminalTools(client, host, runId, turnId, runRoot, input, observed, persistBatch, pendingBatch, flushEvents) {
910
+ const definitions = [];
911
+ const add = (definition) => {
912
+ definitions.push(definition);
913
+ };
914
+ add({
915
+ name: input.executionPurpose === "RELEASE"
916
+ ? "submit_pull_requests"
917
+ : input.phase === "DEVELOPMENT"
918
+ ? "complete_development"
919
+ : "submit_delivery",
920
+ label: "提交交付",
921
+ description: input.executionPurpose === "RELEASE"
922
+ ? "核实上线清单并提交分支后,使用 Runtime GitLab 身份创建或复用 MR 并登记工单。"
923
+ : "读取并校验本阶段最终交付文件,上传成功后完成当前阶段。",
924
+ parameters: input.executionPurpose === "RELEASE"
925
+ ? Type.Object({
926
+ checklistSummary: Type.String({
927
+ description: "核实依据、无需修改或修改摘要;缺口未解决时不要提交",
928
+ }),
929
+ projects: Type.Array(Type.Object({
930
+ project: Type.String({
931
+ description: `项目名称,只能选择:${input.projects.map((p) => p.name).join("、")}`,
932
+ }),
933
+ title: Type.String({ description: "包含工单编号及本项目变更摘要的 MR 标题" }),
934
+ description: Type.String({
935
+ description: "任务目的、本项目改动、测试与验收结论、发布注意、依赖、风险回滚及清单和报告链接",
936
+ }),
937
+ })),
938
+ })
939
+ : Type.Object({}),
940
+ async execute(_toolCallId, rawParams) {
941
+ try {
942
+ if (observed.activeTasks.size > 0)
943
+ throw new Error("SUBAGENT_TASKS_RUNNING:子任务尚未结束,请等待结果后再提交阶段交付。");
944
+ if (input.executionPurpose === "RELEASE") {
945
+ const params = rawParams;
946
+ if (!params?.checklistSummary?.trim())
947
+ throw new Error("AIP_RELEASE_CHECKLIST_SUMMARY_REQUIRED");
948
+ const files = await collectDeliveryFiles(runRoot, input, ["RELEASE_CHECKLIST"]);
949
+ const checklist = {
950
+ status: "VERIFIED",
951
+ summary: params.checklistSummary,
952
+ markdown: files[0].content,
953
+ };
954
+ await client.submitPullRequests(runId, {
955
+ hostId: host.hostId,
956
+ generation: host.generation,
957
+ turnId,
958
+ workItemId: input.workItem.id,
959
+ projects: [],
960
+ checklist,
961
+ });
962
+ const context = {
963
+ runId,
964
+ turnId,
965
+ workItem: input.workItem,
966
+ projects: input.projects,
967
+ primaryProjectId: input.primaryProjectId,
968
+ };
969
+ for (const project of input.projects) {
970
+ const explanation = params.projects?.find((item) => item.project === project.name);
971
+ if (!explanation)
972
+ throw new Error(`AIP_MR_DESCRIPTION_REQUIRED:${project.name}`);
973
+ const pendingFile = join(runRoot, `mr-pending-${safeName(project.id)}.json`);
974
+ const result = await prepareMergeRequest({
975
+ project,
976
+ cwd: join(runRoot, safeName(project.name)),
977
+ title: explanation.title,
978
+ description: explanation.description,
979
+ pendingFile,
980
+ });
981
+ await registerMergeRequest(client, context, host, result, pendingFile);
982
+ }
983
+ await client.submitPullRequests(runId, {
984
+ hostId: host.hostId,
985
+ generation: host.generation,
986
+ turnId,
987
+ workItemId: input.workItem.id,
988
+ projects: [],
989
+ completeRelease: true,
990
+ });
991
+ observed.terminalReached = true;
992
+ return toolResult("上线清单已核实,MR 已提交并登记,等待合并;平台负责查询最新状态。");
993
+ }
994
+ const existing = pendingBatch();
995
+ if (existing && existing.turnId === turnId) {
996
+ await flushEvents();
997
+ await uploadBatch(client, runId, existing);
998
+ await persistBatch(undefined);
999
+ observed.terminalReached = true;
1000
+ return toolResult("交付已提交,等待经办人确认。");
1001
+ }
1002
+ const files = await collectDeliveryFiles(runRoot, input, input.phase === "DEVELOPMENT"
1003
+ ? Object.keys(DELIVERY_FILES).filter((slot) => STAGE_DELIVERY_PROTOCOLS.DEVELOPMENT.requiredFiles.includes(DELIVERY_FILES[slot]))
1004
+ : undefined);
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);
1008
+ const batch = {
1009
+ hostId: host.hostId,
1010
+ generation: host.generation,
1011
+ turnId,
1012
+ kind: input.phase === "DEVELOPMENT" ? "DEVELOPMENT_COMPLETED" : "DELIVERY",
1013
+ submissionId: crypto.randomUUID(),
1014
+ files,
1015
+ summary: files.find((file) => file.slot === "SUMMARY").content,
1016
+ knownIssues: "",
1017
+ projectFacts,
1018
+ testFacts: [],
1019
+ };
1020
+ await persistBatch(batch);
1021
+ await flushEvents();
1022
+ await uploadBatch(client, runId, batch);
1023
+ await persistBatch(undefined);
1024
+ observed.terminalReached = true;
1025
+ return toolResult("交付已提交,等待经办人确认。");
1026
+ }
1027
+ catch (error) {
1028
+ observed.lastDeliveryError = error instanceof Error ? error.message : "AIP_DELIVERY_FAILED";
1029
+ if (error instanceof PlatformClientError && error.status < 500 && error.status !== 429)
1030
+ await persistBatch(undefined);
1031
+ throw error;
1032
+ }
1033
+ },
1034
+ });
1035
+ if (input.executionPurpose === "RELEASE")
1036
+ add({
1037
+ name: "request_code_changes",
1038
+ label: "返回开发修改",
1039
+ description: "用户明确要求修改应用代码时,将工单返回开发,再测试和人工确认;仅讨论或文档修改不要调用。",
1040
+ parameters: Type.Object({
1041
+ reason: Type.String({ description: "用户要求修改的内容及原因" }),
1042
+ }),
1043
+ async execute(_id, params) {
1044
+ await client.request(`/api/v1/coding-runs/${runId}/return-to-development`, {
1045
+ method: "POST",
1046
+ body: JSON.stringify({
1047
+ hostId: host.hostId,
1048
+ generation: host.generation,
1049
+ turnId,
1050
+ reason: params.reason,
1051
+ }),
1052
+ });
1053
+ observed.terminalReached = true;
1054
+ return toolResult("已返回开发,等待下一执行接手续改,完成后进入测试与人工确认。");
1055
+ },
1056
+ });
1057
+ return (api) => {
1058
+ for (const definition of definitions)
1059
+ api.registerTool({
1060
+ name: definition.name,
1061
+ label: definition.label,
1062
+ description: definition.description,
1063
+ parameters: definition.parameters,
1064
+ async execute(id, args, signal) {
1065
+ signal?.throwIfAborted();
1066
+ return definition.execute(id, args);
1067
+ },
1068
+ });
1069
+ };
1070
+ }
1071
+ export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects, resume) {
863
1072
  await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
864
1073
  for (const project of projects) {
865
1074
  const mirror = join(cacheRoot, `${project.id}.git`);
@@ -903,7 +1112,7 @@ export async function prepareProjects(cacheRoot, runRoot, runId, projects, resum
903
1112
  });
904
1113
  }
905
1114
  const target = join(runRoot, safeName(project.name));
906
- const expectedBranch = project.devBranch || `aip/${runId}/${project.id.slice(0, 8)}`;
1115
+ const expectedBranch = project.devBranch || `aip/${workItemKey}`;
907
1116
  const existingWorktree = await exists(target);
908
1117
  if (!existingWorktree) {
909
1118
  if (resume)
@@ -963,22 +1172,40 @@ async function canonicalRepository(value) {
963
1172
  }
964
1173
  return value.replace(/\/$/u, "");
965
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
+ }
966
1189
  export function initialPrompt(input, runRoot, responsible = true) {
967
1190
  const directory = deliveryDirectory(runRoot, input);
968
1191
  if (input.executionPurpose === "RELEASE")
969
- 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。平台查询状态,不负责创建。仅说明或更新文档不退回研发;不执行实际发布、知识整理、关单。`;
970
1193
  const projects = input.projects
971
1194
  .map((p) => `- ${p.name}: ${join(runRoot, safeName(p.name))}(开发分支 ${p.devBranch} → 基准分支 ${p.baselineBranch};测试分支 ${p.testBranch})${p.id === input.primaryProjectId ? "(主项目)" : ""}`)
972
1195
  .join("\n");
973
1196
  const phase = input.phase === "DEVELOPMENT" ? "DEVELOPMENT" : "INTEGRATION";
1197
+ const phaseLabel = phase === "DEVELOPMENT" ? "开发与自审" : "系统测试,当前已进入系统测试,不是开发阶段";
974
1198
  const protocol = STAGE_DELIVERY_PROTOCOLS[phase];
975
1199
  const delivery = input.stageDeliverySnapshots?.[phase];
976
1200
  if (responsible && !delivery)
977
1201
  throw new Error("STAGE_DELIVERY_CONFIGURATION_MISSING");
978
- 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;本地副本未被覆盖,对照文件不收集。` : ""}`;
979
1203
  if (!responsible)
980
1204
  return context;
981
- 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}`;
982
1209
  }
983
1210
  async function uploadBatch(client, runId, batch) {
984
1211
  for (let attempt = 0;; attempt++) {
@@ -995,24 +1222,6 @@ async function uploadBatch(client, runId, batch) {
995
1222
  }
996
1223
  }
997
1224
  }
998
- function textFromMessage(content) {
999
- if (typeof content === "string")
1000
- return content;
1001
- if (!Array.isArray(content))
1002
- return "";
1003
- return content
1004
- .filter((part) => Boolean(part &&
1005
- typeof part === "object" &&
1006
- part.type === "text" &&
1007
- typeof part.text === "string"))
1008
- .map((part) => part.text)
1009
- .join("\n");
1010
- }
1011
- function textFromResult(value) {
1012
- if (!value || typeof value !== "object")
1013
- return "";
1014
- return textFromMessage(value.content).slice(0, 4000);
1015
- }
1016
1225
  function toolResult(text) {
1017
1226
  return { content: [{ type: "text", text }], details: {} };
1018
1227
  }
@@ -1047,6 +1256,29 @@ async function collectProjectFacts(runRoot, projects, key, primaryProjectId) {
1047
1256
  };
1048
1257
  }));
1049
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
+ }
1050
1282
  async function captureProjectRevisions(runRoot, projects, key, primaryProjectId) {
1051
1283
  return collectProjectFacts(runRoot, projects, key, primaryProjectId);
1052
1284
  }