@bf-meishijian/aip 0.0.5 → 0.0.7

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 (143) hide show
  1. package/README.md +114 -3
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +57 -23
  5. package/dist/cli.js.map +1 -1
  6. package/dist/coding-run-manager.d.ts +83 -17
  7. package/dist/coding-run-manager.d.ts.map +1 -1
  8. package/dist/coding-run-manager.js +861 -644
  9. package/dist/coding-run-manager.js.map +1 -1
  10. package/dist/coding-subagent-child.d.ts +22 -0
  11. package/dist/coding-subagent-child.d.ts.map +1 -0
  12. package/dist/coding-subagent-child.js +83 -0
  13. package/dist/coding-subagent-child.js.map +1 -0
  14. package/dist/coding-subagents.d.ts +65 -0
  15. package/dist/coding-subagents.d.ts.map +1 -0
  16. package/dist/coding-subagents.js +521 -0
  17. package/dist/coding-subagents.js.map +1 -0
  18. package/dist/command-wakeup.d.ts +19 -0
  19. package/dist/command-wakeup.d.ts.map +1 -0
  20. package/dist/command-wakeup.js +109 -0
  21. package/dist/command-wakeup.js.map +1 -0
  22. package/dist/daemon.d.ts +4 -1
  23. package/dist/daemon.d.ts.map +1 -1
  24. package/dist/daemon.js +189 -79
  25. package/dist/daemon.js.map +1 -1
  26. package/dist/delivery-files.d.ts +24 -9
  27. package/dist/delivery-files.d.ts.map +1 -1
  28. package/dist/delivery-files.js +171 -45
  29. package/dist/delivery-files.js.map +1 -1
  30. package/dist/delivery-tools.d.ts +28 -0
  31. package/dist/delivery-tools.d.ts.map +1 -0
  32. package/dist/delivery-tools.js +167 -0
  33. package/dist/delivery-tools.js.map +1 -0
  34. package/dist/diagnostics.d.ts +1 -0
  35. package/dist/diagnostics.d.ts.map +1 -1
  36. package/dist/diagnostics.js +48 -14
  37. package/dist/diagnostics.js.map +1 -1
  38. package/dist/installer.d.ts +5 -8
  39. package/dist/installer.d.ts.map +1 -1
  40. package/dist/installer.js +155 -65
  41. package/dist/installer.js.map +1 -1
  42. package/dist/linux-service.d.ts +14 -0
  43. package/dist/linux-service.d.ts.map +1 -0
  44. package/dist/linux-service.js +63 -0
  45. package/dist/linux-service.js.map +1 -0
  46. package/dist/live-employee.d.ts +6 -3
  47. package/dist/live-employee.d.ts.map +1 -1
  48. package/dist/live-employee.js +245 -49
  49. package/dist/live-employee.js.map +1 -1
  50. package/dist/local-handoff-workspace.d.ts +20 -0
  51. package/dist/local-handoff-workspace.d.ts.map +1 -0
  52. package/dist/local-handoff-workspace.js +80 -0
  53. package/dist/local-handoff-workspace.js.map +1 -0
  54. package/dist/local-model-usage.d.ts +6 -0
  55. package/dist/local-model-usage.d.ts.map +1 -0
  56. package/dist/local-model-usage.js +40 -0
  57. package/dist/local-model-usage.js.map +1 -0
  58. package/dist/login-url.d.ts +3 -0
  59. package/dist/login-url.d.ts.map +1 -0
  60. package/dist/login-url.js +30 -0
  61. package/dist/login-url.js.map +1 -0
  62. package/dist/paths.d.ts +9 -0
  63. package/dist/paths.d.ts.map +1 -1
  64. package/dist/paths.js +27 -3
  65. package/dist/paths.js.map +1 -1
  66. package/dist/pi-message-queue.d.ts +15 -0
  67. package/dist/pi-message-queue.d.ts.map +1 -0
  68. package/dist/pi-message-queue.js +57 -0
  69. package/dist/pi-message-queue.js.map +1 -0
  70. package/dist/pi-model-service.d.ts +11 -0
  71. package/dist/pi-model-service.d.ts.map +1 -1
  72. package/dist/pi-model-service.js +28 -11
  73. package/dist/pi-model-service.js.map +1 -1
  74. package/dist/platform-client.d.ts +230 -6
  75. package/dist/platform-client.d.ts.map +1 -1
  76. package/dist/platform-client.js +87 -0
  77. package/dist/platform-client.js.map +1 -1
  78. package/dist/profile.d.ts +9 -0
  79. package/dist/profile.d.ts.map +1 -0
  80. package/dist/profile.js +43 -0
  81. package/dist/profile.js.map +1 -0
  82. package/dist/project-facts.d.ts +19 -0
  83. package/dist/project-facts.d.ts.map +1 -0
  84. package/dist/project-facts.js +91 -0
  85. package/dist/project-facts.js.map +1 -0
  86. package/dist/provider-auth-error.d.ts +3 -0
  87. package/dist/provider-auth-error.d.ts.map +1 -0
  88. package/dist/provider-auth-error.js +12 -0
  89. package/dist/provider-auth-error.js.map +1 -0
  90. package/dist/qa-contract.d.ts +2 -0
  91. package/dist/qa-contract.d.ts.map +1 -0
  92. package/dist/qa-contract.js +165 -0
  93. package/dist/qa-contract.js.map +1 -0
  94. package/dist/qa-http-validation.js +145 -0
  95. package/dist/qa-project-checkout.d.ts +12 -0
  96. package/dist/qa-project-checkout.d.ts.map +1 -0
  97. package/dist/qa-project-checkout.js +68 -0
  98. package/dist/qa-project-checkout.js.map +1 -0
  99. package/dist/qa-session-manager.d.ts +101 -0
  100. package/dist/qa-session-manager.d.ts.map +1 -0
  101. package/dist/qa-session-manager.js +581 -0
  102. package/dist/qa-session-manager.js.map +1 -0
  103. package/dist/review-report.d.ts +9 -0
  104. package/dist/review-report.d.ts.map +1 -0
  105. package/dist/review-report.js +61 -0
  106. package/dist/review-report.js.map +1 -0
  107. package/dist/secure-json.d.ts.map +1 -1
  108. package/dist/secure-json.js +64 -2
  109. package/dist/secure-json.js.map +1 -1
  110. package/dist/stage-delivery.js +43 -25
  111. package/dist/state.d.ts +2 -2
  112. package/dist/state.d.ts.map +1 -1
  113. package/dist/state.js +6 -4
  114. package/dist/state.js.map +1 -1
  115. package/dist/test-cli.d.ts +2 -0
  116. package/dist/test-cli.d.ts.map +1 -0
  117. package/dist/test-cli.js +50 -0
  118. package/dist/test-cli.js.map +1 -0
  119. package/dist/test-executor.d.ts +44 -0
  120. package/dist/test-executor.d.ts.map +1 -0
  121. package/dist/test-executor.js +474 -0
  122. package/dist/test-executor.js.map +1 -0
  123. package/dist/test-reports.d.ts +13 -0
  124. package/dist/test-reports.d.ts.map +1 -0
  125. package/dist/test-reports.js +129 -0
  126. package/dist/test-reports.js.map +1 -0
  127. package/dist/test-run-manager.d.ts +24 -0
  128. package/dist/test-run-manager.d.ts.map +1 -0
  129. package/dist/test-run-manager.js +121 -0
  130. package/dist/test-run-manager.js.map +1 -0
  131. package/dist/tool-dispatch.d.ts +14 -0
  132. package/dist/tool-dispatch.d.ts.map +1 -0
  133. package/dist/tool-dispatch.js +56 -0
  134. package/dist/tool-dispatch.js.map +1 -0
  135. package/dist/windows-runtime.d.ts +9 -0
  136. package/dist/windows-runtime.d.ts.map +1 -0
  137. package/dist/windows-runtime.js +64 -0
  138. package/dist/windows-runtime.js.map +1 -0
  139. package/dist/windows-service.d.ts +20 -0
  140. package/dist/windows-service.d.ts.map +1 -0
  141. package/dist/windows-service.js +96 -0
  142. package/dist/windows-service.js.map +1 -0
  143. package/package.json +17 -6
@@ -1,18 +1,24 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
- import { mkdir, mkdtemp, open, realpath, rm, stat } from "node:fs/promises";
4
- import { tmpdir } from "node:os";
3
+ import { mkdir, open, realpath, stat } from "node:fs/promises";
5
4
  import { basename, dirname, join, resolve } from "node:path";
6
5
  import { promisify } from "node:util";
7
6
  import { createAgentSession, DefaultResourceLoader, SessionManager, } from "@earendil-works/pi-coding-agent";
8
- import { Type } from "typebox";
9
- import { collectDeliveryFiles, deliveryDirectory, initializeDeliveryFiles, } from "./delivery-files.js";
7
+ import { deliveryTools } from "./delivery-tools.js";
8
+ import { inspectHandoffWorkspace, synchronizeLocalIntake, synchronizeRemoteWorkspace, } from "./local-handoff-workspace.js";
9
+ import { localModelUsage } from "./local-model-usage.js";
10
+ import { collectProjectFacts, worktreeTreeHash } from "./project-facts.js";
11
+ import { isProviderLoginRequired } from "./provider-auth-error.js";
12
+ export { worktreeTreeHash } from "./project-facts.js";
13
+ import { createCodingSubagents } from "./coding-subagents.js";
14
+ import { commandMaterialReader, deliveryDirectory, initializeDeliveryFiles, publishReviewMaterials, } from "./delivery-files.js";
10
15
  import { liveEmployee } from "./live-employee.js";
11
- import { prepareMergeRequest, registerMergeRequest, } from "./merge-requests.js";
12
16
  import { paths } from "./paths.js";
17
+ import { PiMessageQueue } from "./pi-message-queue.js";
13
18
  import { PlatformClientError } from "./platform-client.js";
19
+ import { materializeReviewReport } from "./review-report.js";
14
20
  import { readJson, writeSecureJson } from "./secure-json.js";
15
- import { DELIVERY_FILES, STAGE_DELIVERY_PROTOCOLS, stageEmployeeIds } from "./stage-delivery.js";
21
+ import { stageEmployeeIds } from "./stage-delivery.js";
16
22
  const execFileAsync = promisify(execFile);
17
23
  const EVENT_APPEND_MAX_ATTEMPTS = 8;
18
24
  export class CodingRunManager {
@@ -25,8 +31,63 @@ export class CodingRunManager {
25
31
  return this.active.size > 0;
26
32
  }
27
33
  async execute(client, host, command) {
34
+ const readMaterial = commandMaterialReader((source) => client.commandMaterial(command.id, {
35
+ hostId: host.hostId,
36
+ generation: host.generation,
37
+ contentHash: source.contentHash,
38
+ }));
28
39
  if (!command.run)
29
40
  throw new Error("CODING_RUN_COMMAND_INVALID");
41
+ if (command.run.action === "STOP" && command.run.runInput?.discussionTurn) {
42
+ const discussion = this.active.get(`${command.run.runId}:discussion`);
43
+ if (discussion && discussion.turnId === command.run.turnId) {
44
+ discussion.stopping = true;
45
+ discussion.observed.stopping = true;
46
+ await discussion.subagents?.stop();
47
+ await discussion.session.abort();
48
+ await discussion.emit("RUN_STOPPED", { phase: "DEVELOPMENT" });
49
+ }
50
+ else {
51
+ const stateFile = join(paths().runState, `${command.run.runId}.json`);
52
+ const state = await readJson(stateFile);
53
+ await eventEmitter(client, host, command.run.runId, command.run.turnId, state?.lastEventSequence ?? 0, async (sequence) => {
54
+ const current = await readJson(stateFile);
55
+ if (current)
56
+ await writeSecureJson(stateFile, { ...current, lastEventSequence: sequence });
57
+ }, undefined, true)("RUN_STOPPED", { phase: "DEVELOPMENT" });
58
+ }
59
+ return;
60
+ }
61
+ if (command.run.action === "REVIEW_REPORT") {
62
+ const report = command.run.reviewReport;
63
+ if (!report || !command.run.runInput)
64
+ throw new Error("REVIEW_REPORT_INPUT_MISSING");
65
+ const input = command.run.runInput;
66
+ const runRoot = join(paths().runWorktrees, command.run.runId);
67
+ if (input.transferContinuation) {
68
+ const notes = await inspectPriorTransferWorkspaces(paths().runWorktrees, input.transferWorkspaces?.priorRunIds ?? [], input.projects);
69
+ if (notes.length) {
70
+ const state = await readJson(join(paths().runState, `${command.run.runId}.json`));
71
+ await eventEmitter(client, host, command.run.runId, command.run.turnId, state?.lastEventSequence ?? 0)("MESSAGE_COMPLETED", { contentMarkdown: `旧工作区检查:\n${notes.join("\n")}` });
72
+ }
73
+ await mkdir(runRoot, { recursive: true, mode: 0o700 });
74
+ await prepareProjects(paths().repositoryCache, runRoot, input.workItem.key, input.projects, false, true, input.transferWorkspaces?.uninitializedProjectIds);
75
+ }
76
+ const files = await materializeReviewReport(runRoot, input, report.files, readMaterial);
77
+ await client.acknowledgeReviewReport(command.run.runId, {
78
+ hostId: host.hostId,
79
+ generation: host.generation,
80
+ turnId: command.run.turnId,
81
+ roundId: report.roundId,
82
+ files,
83
+ });
84
+ return;
85
+ }
86
+ if (command.run.action === "RECALL_QUEUE") {
87
+ return {
88
+ recalledMessageIds: this.active.get(command.run.runId)?.observed.messageQueue?.recall() ?? [],
89
+ };
90
+ }
30
91
  const imageRun = command.run;
31
92
  let images;
32
93
  try {
@@ -41,6 +102,7 @@ export class CodingRunManager {
41
102
  if (active?.turnId === imageRun.turnId) {
42
103
  active.stopping = true;
43
104
  active.observed.stopping = true;
105
+ await active.subagents?.stop();
44
106
  await active.session.abort();
45
107
  await active.finished;
46
108
  if (this.active.get(imageRun.runId) === active)
@@ -48,11 +110,32 @@ export class CodingRunManager {
48
110
  }
49
111
  throw error;
50
112
  }
113
+ if (command.run.runInput?.discussionTurn) {
114
+ await this.start(readMaterial, client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
115
+ const discussion = this.active.get(`${command.run.runId}:discussion`);
116
+ if (discussion && command.run.messageId)
117
+ await discussion.emit("MESSAGE_QUEUE_UPDATED", {
118
+ messageId: command.run.messageId,
119
+ deliveryStatus: "READ",
120
+ });
121
+ return;
122
+ }
51
123
  if (command.run.action === "START") {
52
124
  const input = command.run.runInput;
53
- await this.start(client, host, command.run.runId, command.run.turnId, input, false, command.run.contentMarkdown, images);
125
+ await this.start(readMaterial, client, host, command.run.runId, command.run.turnId, input, false, command.run.contentMarkdown, images);
126
+ }
127
+ else if (command.run.action === "CONTINUE") {
128
+ const active = this.active.get(command.run.runId);
129
+ if (active) {
130
+ await active.finished;
131
+ if (active.stopping || active.observed.userIntervened)
132
+ return;
133
+ }
134
+ if (!command.run.runInput || !command.run.contentMarkdown || !command.run.continuation)
135
+ throw new Error("CODING_RUN_CONTINUATION_MISSING");
136
+ await this.start(readMaterial, client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, [], command.run.continuation);
54
137
  }
55
- else if (command.run.action === "STEER") {
138
+ else if (command.run.action === "STEER" || command.run.action === "FOLLOW_UP") {
56
139
  let active = this.active.get(command.run.runId);
57
140
  if (!command.run.contentMarkdown && images.length === 0)
58
141
  throw new Error("CODING_RUN_MESSAGE_MISSING");
@@ -61,25 +144,65 @@ export class CodingRunManager {
61
144
  throw new Error("CODING_RUN_TURN_MISMATCH");
62
145
  active.stopping = true;
63
146
  active.observed.stopping = true;
147
+ await active.emit("EXECUTION_DECIDED", {
148
+ phase: active.input.phase,
149
+ decision: "SKIP",
150
+ reason: "SUPERSEDED",
151
+ stopReason: "SUPERSEDED",
152
+ });
153
+ await active.subagents?.stop();
64
154
  await active.session.abort();
65
155
  if (this.active.get(command.run.runId) === active)
66
156
  this.active.delete(command.run.runId);
67
157
  active = undefined;
68
158
  }
69
159
  if (active) {
160
+ if (command.run.runInput)
161
+ active.input.handoff = command.run.runInput.handoff ?? null;
70
162
  if (images.length > 0 && !active.supportsImages)
71
163
  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 ?? "");
164
+ active.observed.userIntervened = true;
165
+ const handoff = active.observed.awaitingDecision || active.session.isIdle;
166
+ if (handoff)
167
+ active.observed.handingOffInput = true;
168
+ await active.emit("AUTO_CONTINUATION_CHANGED", {
169
+ phase: active.input.phase,
170
+ autoContinueEnabled: false,
171
+ reason: "USER_MESSAGE",
172
+ source: "HOST_HANDLED",
173
+ });
174
+ if (handoff || active.session.isIdle || active.observed.awaitingDecision) {
175
+ active.observed.handingOffInput = true;
176
+ // Pi is idle: steer only queues input and cannot start a new model turn.
177
+ // Finish the old judge operation before opening the user turn.
178
+ await active.finished;
179
+ if (!command.run.runInput)
180
+ throw new Error("CODING_RUN_INPUT_MISSING");
181
+ await this.start(readMaterial, client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
182
+ if (command.run.messageId)
183
+ await this.active.get(command.run.runId)?.emit("MESSAGE_QUEUE_UPDATED", {
184
+ messageId: command.run.messageId,
185
+ deliveryStatus: "READ",
186
+ });
187
+ return;
188
+ }
189
+ if (!command.run.messageId)
190
+ throw new Error("CODING_RUN_MESSAGE_ID_MISSING");
191
+ await active.observed.messageQueue.submit(command.run.messageId, command.run.action, command.run.contentMarkdown ?? "", images.length ? images : undefined);
192
+ await active.emit("MESSAGE_QUEUE_UPDATED", {
193
+ messageId: command.run.messageId,
194
+ deliveryStatus: "QUEUED",
195
+ });
78
196
  }
79
197
  else {
80
198
  if (!command.run.runInput)
81
199
  throw new Error("CODING_RUN_INPUT_MISSING");
82
- await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
200
+ await this.start(readMaterial, client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
201
+ if (command.run.messageId)
202
+ await this.active.get(command.run.runId)?.emit("MESSAGE_QUEUE_UPDATED", {
203
+ messageId: command.run.messageId,
204
+ deliveryStatus: "READ",
205
+ });
83
206
  }
84
207
  }
85
208
  else if (command.run.action === "STOP") {
@@ -89,45 +212,95 @@ export class CodingRunManager {
89
212
  throw new Error("CODING_RUN_TURN_MISMATCH");
90
213
  active.stopping = true;
91
214
  active.observed.stopping = true;
215
+ await active.subagents?.stop();
92
216
  await active.session.abort();
217
+ await active.emit("AUTO_CONTINUATION_CHANGED", {
218
+ phase: active.input.phase,
219
+ autoContinueEnabled: false,
220
+ reason: "USER_STOP",
221
+ source: "HOST_HANDLED",
222
+ });
93
223
  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", {});
224
+ await active.emit("EXECUTION_DECIDED", {
225
+ phase: active.input.phase,
226
+ decision: "SKIP",
227
+ reason: "STOP_REQUESTED",
228
+ stopReason: "USER_STOPPED",
229
+ });
230
+ await active.emit("RUN_STOPPED", {
231
+ phase: active.input.phase,
232
+ reason: "USER_STOPPED",
233
+ workspaceChanges: await Promise.all(active.input.projects.map(async (project) => ({
234
+ projectId: project.id,
235
+ ...(await inspectHandoffWorkspace(join(active.runRoot, safeName(project.name)))),
236
+ }))),
237
+ });
103
238
  }
104
239
  else {
105
240
  const stateFile = join(paths().runState, `${command.run.runId}.json`);
106
241
  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", {});
242
+ 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", {
243
+ phase: state?.phase,
244
+ reason: "USER_STOPPED",
245
+ reminderDecision: "NO_REMINDER",
246
+ workspaceChanges: state?.mrContext?.projects
247
+ ? await Promise.all(state.mrContext.projects.map(async (project) => ({
248
+ projectId: project.id,
249
+ ...(await inspectHandoffWorkspace(join(paths().runWorktrees, command.run.runId, safeName(project.name)))),
250
+ })))
251
+ : [],
252
+ });
108
253
  }
109
254
  }
110
255
  else if (command.run.action === "RESUME") {
111
- await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
256
+ await this.start(readMaterial, client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
257
+ }
258
+ if (command.run.messageId &&
259
+ ["START", "RESUME", "STEER", "FOLLOW_UP"].includes(command.run.action)) {
260
+ const active = this.active.get(command.run.runId);
261
+ if (active &&
262
+ (command.run.action === "RESUME" ||
263
+ command.run.action === "START" ||
264
+ !active.observed.messageQueue))
265
+ await active.emit("MESSAGE_QUEUE_UPDATED", {
266
+ messageId: command.run.messageId,
267
+ deliveryStatus: "READ",
268
+ });
112
269
  }
113
270
  }
114
- async start(client, host, runId, turnId, input, resume, resumePrompt, images = []) {
115
- const active = this.active.get(runId);
271
+ async start(readMaterial, client, host, runId, turnId, input, resume, resumePrompt, images = [], continuation) {
272
+ if (!input.discussionTurn && input.phase === "DEVELOPMENT") {
273
+ const discussion = this.active.get(`${runId}:discussion`);
274
+ if (discussion) {
275
+ discussion.stopping = true;
276
+ discussion.observed.stopping = true;
277
+ await discussion.subagents?.stop();
278
+ await discussion.session.abort();
279
+ await discussion.emit("RUN_STOPPED", { phase: "DEVELOPMENT" });
280
+ await discussion.finished;
281
+ }
282
+ }
283
+ const activeKey = input.discussionTurn ? `${runId}:discussion` : runId;
284
+ const active = this.active.get(activeKey);
116
285
  if (active) {
117
286
  if (active.turnId === turnId)
118
287
  return;
119
- if (!active.observed.terminalReached)
288
+ if (!active.observed.terminalReached && !active.stopping)
120
289
  throw new Error("CODING_RUN_TURN_MISMATCH");
121
290
  await active.finished;
122
- if (this.active.get(runId) === active)
123
- this.active.delete(runId);
291
+ if (this.active.get(activeKey) === active)
292
+ this.active.delete(activeKey);
124
293
  }
125
- if (process.env.AIP_TEST_PI === "1") {
294
+ if (process.env.AIP_TEST_PI === "1" && !input.discussionTurn) {
126
295
  await this.synthetic(client, host, runId, turnId, input);
127
296
  return;
128
297
  }
129
298
  const local = paths();
130
- const runRoot = join(local.runWorktrees, runId);
299
+ const developmentRoot = join(local.runWorktrees, runId);
300
+ const deploymentSession = input.phase === "DEPLOYMENT" && input.executionPurpose !== "RELEASE";
301
+ const runRoot = deploymentSession
302
+ ? join(local.root, "workspaces", "deployments", runId)
303
+ : developmentRoot;
131
304
  await mkdir(runRoot, { recursive: true, mode: 0o700 });
132
305
  const stateFile = join(local.runState, `${runId}.json`);
133
306
  const storedState = await readJson(stateFile);
@@ -135,20 +308,56 @@ export class CodingRunManager {
135
308
  Object.keys(storedState.phaseSessions).length &&
136
309
  storedState.harness !== "pi")
137
310
  throw new Error("PRIOR_HARNESS_SESSION_UNSUPPORTED:请新建执行,历史文件和工作目录已保留。");
138
- const cwd = await prepareProjects(local.repositoryCache, runRoot, runId, input.projects, Boolean(storedState?.prepared));
139
311
  for (const project of input.projects)
140
- project.devBranch ||= `aip/${runId}/${project.id.slice(0, 8)}`;
141
- if (!storedState?.prepared || input.inputConflict)
142
- await initializeDeliveryFiles(runRoot, input, Boolean(storedState?.prepared));
312
+ project.devBranch ||=
313
+ storedState?.mrContext?.projects.find((p) => p.id === project.id)?.devBranch ||
314
+ `aip/${input.workItem.key}`;
315
+ const transferNotes = input.transferContinuation && !storedState?.prepared
316
+ ? await inspectPriorTransferWorkspaces(local.runWorktrees, input.transferWorkspaces?.priorRunIds ?? [], input.projects)
317
+ : [];
318
+ const cwd = input.discussionTurn
319
+ ? input.projects.length === 1
320
+ ? join(runRoot, safeName(input.projects[0].name))
321
+ : runRoot
322
+ : deploymentSession
323
+ ? await prepareDeploymentProjects(runRoot, input.projects, input.deploymentConfiguration)
324
+ : await prepareProjects(local.repositoryCache, runRoot, input.workItem.key, input.projects, Boolean(storedState?.prepared), input.transferContinuation === true, input.transferWorkspaces?.uninitializedProjectIds);
325
+ let appliedLocalIntake = storedState?.appliedLocalIntake;
326
+ if (!input.discussionTurn && input.localIntake && appliedLocalIntake !== input.localIntake.id) {
327
+ for (const project of input.projects) {
328
+ const accepted = input.localIntake.projects.find((fact) => fact.projectId === project.id);
329
+ if (!accepted || accepted.branch !== project.devBranch)
330
+ throw new Error("LOCAL_INTAKE_PROJECT_MISMATCH");
331
+ await synchronizeLocalIntake(join(developmentRoot, safeName(project.name)), accepted.commitSha);
332
+ }
333
+ appliedLocalIntake = input.localIntake.id;
334
+ await writeSecureJson(stateFile, { ...storedState, appliedLocalIntake });
335
+ }
336
+ const preparationBaselines = storedState?.preparationBaselines ?? {};
337
+ if (input.phase === "PREPARATION" && !storedState?.prepared) {
338
+ for (const project of input.projects)
339
+ preparationBaselines[project.id] ??= (await execFileAsync("git", ["rev-parse", "HEAD"], {
340
+ cwd: join(runRoot, safeName(project.name)),
341
+ })).stdout.trim();
342
+ await writeSecureJson(stateFile, { ...storedState, preparationBaselines });
343
+ }
344
+ await initializeDeliveryFiles(runRoot, input, Boolean(storedState?.prepared), readMaterial);
143
345
  const deliveryInputHash = createHash("sha256")
144
346
  .update(JSON.stringify([
145
347
  input.primaryProjectId,
146
- input.requirement.body_markdown,
147
- input.technicalDesign?.content,
348
+ input.requirement.source.contentHash,
349
+ input.technicalDesign?.source.contentHash,
148
350
  ]))
149
351
  .digest("hex");
150
- const childSessions = storedState?.childSessions ?? {};
151
- let pendingDelivery = storedState?.phase !== input.phase ||
352
+ const newDeploymentRequest = deploymentSession &&
353
+ Boolean(input.deploymentRequestId) &&
354
+ input.deploymentRequestId !== storedState?.deploymentRequestId;
355
+ const deploymentRequestId = deploymentSession
356
+ ? input.deploymentRequestId
357
+ : storedState?.deploymentRequestId;
358
+ const delegations = storedState?.delegations ?? {};
359
+ let pendingDelivery = newDeploymentRequest ||
360
+ storedState?.phase !== input.phase ||
152
361
  resumePrompt ||
153
362
  images.length > 0 ||
154
363
  (storedState?.deliveryInputHash && storedState.deliveryInputHash !== deliveryInputHash)
@@ -163,39 +372,54 @@ export class CodingRunManager {
163
372
  : undefined;
164
373
  const phaseSessions = storedState?.phaseSessions ?? {};
165
374
  const sessionPhase = input.executionPurpose === "RELEASE" ? "DEVELOPMENT" : input.phase;
166
- let sessionRef = phaseSessions[sessionPhase];
375
+ let sessionRef = newDeploymentRequest ? undefined : phaseSessions[sessionPhase];
167
376
  let lastEventSequence = storedState?.lastEventSequence ?? 0;
168
377
  const observed = {
169
- pendingQuestion: storedState?.phase === input.phase ? (storedState.pendingQuestion ?? "") : "",
170
- memberResults: new Map(),
378
+ userIntervened: input.autoContinueEnabled === false,
171
379
  activeTasks: new Map(),
172
380
  stopping: false,
173
381
  terminalReached: false,
174
382
  };
175
- const persistState = () => writeSecureJson(stateFile, {
176
- phase: input.phase,
177
- mrContext: {
178
- runId,
179
- turnId,
180
- workItem: input.workItem,
181
- projects: input.projects,
182
- primaryProjectId: input.primaryProjectId,
183
- },
184
- prepared: true,
185
- phaseSessions,
186
- sessionRef,
187
- harness: "pi",
188
- lastEventSequence,
189
- pendingQuestion: observed.pendingQuestion,
190
- childSessions,
191
- pendingDelivery,
192
- deliveryInputHash,
193
- });
383
+ const persistState = () => input.discussionTurn
384
+ ? Promise.resolve()
385
+ : writeSecureJson(stateFile, {
386
+ phase: input.phase,
387
+ appliedLocalIntake,
388
+ mrContext: {
389
+ runId,
390
+ turnId,
391
+ workItem: input.workItem,
392
+ projects: input.projects,
393
+ primaryProjectId: input.primaryProjectId,
394
+ },
395
+ prepared: true,
396
+ preparationBaselines,
397
+ phaseSessions,
398
+ deploymentRequestId,
399
+ sessionRef,
400
+ harness: "pi",
401
+ lastEventSequence,
402
+ delegations,
403
+ pendingDelivery,
404
+ deliveryInputHash,
405
+ });
194
406
  const redact = (value) => redactHostText(value, [runRoot, local.root]);
195
- const emit = eventEmitter(client, host, runId, turnId, lastEventSequence, (sequence) => {
196
- lastEventSequence = sequence;
197
- return persistState();
198
- }, redact);
407
+ const emit = eventEmitter(client, host, runId, turnId, lastEventSequence, input.discussionTurn
408
+ ? async (sequence) => {
409
+ const current = await readJson(stateFile);
410
+ if (current)
411
+ await writeSecureJson(stateFile, { ...current, lastEventSequence: sequence });
412
+ }
413
+ : (sequence) => {
414
+ lastEventSequence = sequence;
415
+ return persistState();
416
+ }, redact, Boolean(input.discussionTurn));
417
+ if (transferNotes.length)
418
+ await emit("MESSAGE_COMPLETED", {
419
+ contentMarkdown: `旧工作区检查:\n${transferNotes.join("\n")}`,
420
+ });
421
+ if (!storedState?.prepared)
422
+ await publishReviewMaterials(runRoot, input, readMaterial);
199
423
  if (input.phase === "PREPARATION") {
200
424
  await persistState();
201
425
  await emit("PREPARATION_COMPLETED", {
@@ -203,9 +427,7 @@ export class CodingRunManager {
203
427
  projects: await Promise.all(input.projects.map(async (p) => ({
204
428
  projectId: p.id,
205
429
  projectName: p.name,
206
- baselineCommit: (await execFileAsync("git", ["rev-parse", "HEAD"], {
207
- cwd: join(runRoot, safeName(p.name)),
208
- })).stdout.trim(),
430
+ baselineCommit: preparationBaselines[p.id] ?? p.baselineCommit,
209
431
  directory: join(runRoot, safeName(p.name)),
210
432
  branch: (await execFileAsync("git", ["branch", "--show-current"], {
211
433
  cwd: join(runRoot, safeName(p.name)),
@@ -214,10 +436,20 @@ export class CodingRunManager {
214
436
  });
215
437
  return;
216
438
  }
217
- const terminal = terminalTools(client, host, runId, turnId, runRoot, input, observed, redact, async (batch) => {
439
+ const binding = {
440
+ hostId: host.hostId,
441
+ generation: host.generation,
442
+ turnId,
443
+ member: "supervisor",
444
+ };
445
+ const initialEmployeeConfig = await client.employeeConfig(runId, {
446
+ ...binding,
447
+ purpose: "LOAD",
448
+ });
449
+ const terminal = deliveryTools(client, host, runId, turnId, developmentRoot, input, observed, async (batch) => {
218
450
  pendingDelivery = batch;
219
451
  await persistState();
220
- }, () => pendingDelivery, (question) => emit("QUESTION_UPDATED", { question: redact(question) }), () => emit.flush());
452
+ }, () => pendingDelivery, () => emit.flush(), initialEmployeeConfig.toolDefinitions);
221
453
  if (!this.pi.resolveRuntimeModel)
222
454
  throw new Error("MODEL_RUNTIME_UNAVAILABLE");
223
455
  const resolved = await this.pi.resolveRuntimeModel(input.providerId, input.modelId, {
@@ -228,126 +460,43 @@ export class CodingRunManager {
228
460
  });
229
461
  if (images.length > 0 && !resolved.model.input?.includes("image"))
230
462
  throw new Error("MODEL_IMAGES_UNSUPPORTED");
231
- const binding = {
232
- hostId: host.hostId,
233
- generation: host.generation,
234
- turnId,
235
- member: "supervisor",
236
- };
237
463
  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
- }
464
+ const live = liveEmployee(client, runId, binding, initialEmployeeConfig, runRoot, Boolean(input.team), input.executionPurpose === "RELEASE", input.phase, () => observed.terminalReached, () => executionInstructions(input, runRoot), () => {
465
+ observed.terminalClosed = true;
466
+ });
467
+ const delegation = input.executionPurpose === "RELEASE"
468
+ ? undefined
469
+ : await createCodingSubagents({
470
+ client,
471
+ toolDefinitions: initialEmployeeConfig.toolDefinitions,
472
+ config: {
473
+ runId,
474
+ turnId,
475
+ hostId: host.hostId,
476
+ generation: host.generation,
477
+ runRoot,
478
+ providerId: input.providerId,
479
+ modelId: input.modelId,
480
+ phase: input.phase,
481
+ },
482
+ cwd,
483
+ directory: join(local.runState, runId, "subagents", input.phase),
484
+ team: Boolean(input.team),
485
+ agents: availableAgents.map((id) => ({
486
+ id,
487
+ name: input.teamEmployees?.[id]?.name ?? id,
488
+ description: input.teamEmployees?.[id]?.description ?? "",
489
+ })),
490
+ records: delegations,
491
+ emit,
492
+ persist: persistState,
493
+ started(activityKey, agentId) {
494
+ observed.activeTasks.set(activityKey, agentId);
495
+ },
496
+ async completed(activityKey) {
497
+ observed.activeTasks.delete(activityKey);
348
498
  },
349
499
  });
350
- };
351
500
  const opened = await openPiSession({
352
501
  cwd,
353
502
  sessionDir: local.piSessions,
@@ -356,7 +505,7 @@ export class CodingRunManager {
356
505
  modelRuntime: resolved.runtime,
357
506
  model: resolved.model,
358
507
  thinkingLevel: input.thinkingLevel,
359
- extensions: [...(availableAgents.length ? [memberExtension] : []), terminal, live.extension],
508
+ extensions: [...(delegation ? [delegation.extension] : []), terminal, live.extension],
360
509
  systemPrompt: input.team
361
510
  ? "当前为团队主负责人会话,员工配置由平台下发。"
362
511
  : "当前为工单主会话,员工配置由平台下发。",
@@ -365,6 +514,7 @@ export class CodingRunManager {
365
514
  live.attach(opened.session);
366
515
  const session = opened.session;
367
516
  sessionRef = opened.sessionId;
517
+ observed.messageQueue = new PiMessageQueue(session);
368
518
  subscribeSession(session, (kind, payload) => {
369
519
  const activityKey = String(payload.activityKey ?? "");
370
520
  if (kind === "SUBAGENT_STARTED")
@@ -379,6 +529,7 @@ export class CodingRunManager {
379
529
  });
380
530
  }, observed, redact);
381
531
  const activeRun = {
532
+ runRoot,
382
533
  session,
383
534
  supportsImages: resolved.model.input?.includes("image") === true,
384
535
  input,
@@ -386,31 +537,96 @@ export class CodingRunManager {
386
537
  emit,
387
538
  stopping: false,
388
539
  observed,
540
+ ...(delegation ? { subagents: delegation } : {}),
389
541
  };
390
- this.active.set(runId, activeRun);
391
- phaseSessions[sessionPhase] = sessionRef;
392
- await persistState();
393
- await emit("RUN_STARTED", { sessionRef, phase: input.phase });
394
- if (resumePrompt || images.length > 0) {
395
- await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
396
- await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
542
+ const disposeRun = async () => {
543
+ try {
544
+ try {
545
+ await delegation?.stop();
546
+ await delegation?.flush();
547
+ }
548
+ finally {
549
+ await delegation?.dispose();
550
+ await session.extensionRunner?.emit({ type: "session_shutdown", reason: "quit" });
551
+ }
552
+ }
553
+ finally {
554
+ session.dispose();
555
+ if (this.active.get(activeKey) === activeRun)
556
+ this.active.delete(activeKey);
557
+ }
558
+ };
559
+ this.active.set(activeKey, activeRun);
560
+ let conversationFingerprint;
561
+ let prompt;
562
+ try {
563
+ phaseSessions[sessionPhase] = sessionRef;
564
+ await persistState();
565
+ await emit("RUN_STARTED", { sessionRef, phase: input.phase });
566
+ if (!input.discussionTurn &&
567
+ !continuation &&
568
+ (input.autoContinueEnabled === false || (!resume && input.autoContinueEnabled === true))) {
569
+ await emit("AUTO_CONTINUATION_CHANGED", {
570
+ phase: input.phase,
571
+ autoContinueEnabled: input.autoContinueEnabled === true,
572
+ reason: input.autoContinueEnabled === true ? "SYSTEM_STAGE" : "USER_MESSAGE",
573
+ source: "HOST_HANDLED",
574
+ });
575
+ }
576
+ if (!input.discussionTurn && !continuation && (resumePrompt || images.length > 0)) {
577
+ await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
578
+ await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
579
+ }
580
+ conversationFingerprint = await workspaceFingerprint(runRoot, input);
581
+ prompt = continuation
582
+ ? resumePrompt
583
+ : `${initialPrompt(input, runRoot, true, developmentRoot)}${transferNotes.length ? `\n\n旧工作区检查:\n${transferNotes.join("\n")}` : ""}${resumePrompt ? `\n\n用户本次消息:${resumePrompt}` : resume && !resumePrompt ? "\n\n继续当前会话未完成的工作,先读取最新文件和交接结果。" : ""}`;
584
+ }
585
+ catch (error) {
586
+ await disposeRun();
587
+ throw error;
397
588
  }
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)
589
+ const discussion = async () => {
590
+ await session.prompt(prompt, images.length ? { images } : undefined);
591
+ await session.waitForIdle();
592
+ if (!observed.stopping)
593
+ await emit("CONVERSATION_COMPLETED", {
594
+ phase: "DEVELOPMENT",
595
+ workspaceChanged: conversationFingerprint !== (await workspaceFingerprint(runRoot, input)),
596
+ });
597
+ };
598
+ activeRun.finished = (input.discussionTurn
599
+ ? discussion()
600
+ : driveToTerminal(session, prompt, observed, client, host, runId, turnId, emit, input.phase, images, async () => {
601
+ await emit("CONVERSATION_COMPLETED", {
602
+ workspaceChanged: conversationFingerprint !== (await workspaceFingerprint(runRoot, input)),
603
+ });
604
+ }, continuation))
400
605
  .catch(async (error) => {
401
606
  if (activeRun.stopping)
402
607
  return;
608
+ if (!input.discussionTurn)
609
+ await emit("EXECUTION_DECIDED", {
610
+ phase: input.phase,
611
+ decision: "SKIP",
612
+ reason: "MODEL_ERROR",
613
+ stopReason: "error",
614
+ });
403
615
  await emit("RUN_FAILED", {
404
- code: error instanceof Error ? error.message.slice(0, 200) : "PI_RUN_FAILED",
616
+ phase: input.phase,
617
+ code: error instanceof Error && /^[A-Z][A-Z0-9_]+$/u.test(error.message)
618
+ ? error.message.slice(0, 200)
619
+ : "PI_RUN_FAILED",
620
+ message: redact(error instanceof Error ? error.message : String(error)),
405
621
  });
406
622
  })
407
623
  .finally(async () => {
408
- await emit.flush();
409
- for (const child of childInstances.values())
410
- child.dispose();
411
- session.dispose();
412
- if (this.active.get(runId) === activeRun)
413
- this.active.delete(runId);
624
+ try {
625
+ await emit.flush();
626
+ }
627
+ finally {
628
+ await disposeRun();
629
+ }
414
630
  });
415
631
  }
416
632
  async synthetic(client, host, runId, turnId, input) {
@@ -446,25 +662,33 @@ export class CodingRunManager {
446
662
  treeHash: "a".repeat(40),
447
663
  branch: project.testBranch,
448
664
  })),
449
- testFacts: input.projects.map((project) => ({
450
- projectId: project.id,
451
- command: "pnpm test",
452
- exitCode: 0,
453
- commitSha: project.baselineCommit,
454
- treeHash: "a".repeat(40),
455
- finishedAt: new Date().toISOString(),
456
- })),
665
+ testFacts: [],
457
666
  });
458
667
  }
459
668
  }
460
- function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistSequence, redact = (value) => redactHostText(value)) {
461
- let sequence = initialSequence;
462
- let queue = Promise.resolve();
669
+ const eventStreams = new WeakMap();
670
+ function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistSequence, redact = (value) => redactHostText(value), preservePersist = false) {
671
+ let streams = eventStreams.get(client);
672
+ if (!streams) {
673
+ streams = new Map();
674
+ eventStreams.set(client, streams);
675
+ }
676
+ let stream = streams.get(runId);
677
+ if (!stream) {
678
+ stream = { sequence: initialSequence, queue: Promise.resolve() };
679
+ streams.set(runId, stream);
680
+ }
681
+ stream.sequence = Math.max(stream.sequence, initialSequence);
682
+ if (persistSequence && (!preservePersist || !stream.persist))
683
+ stream.persist = persistSequence;
684
+ const state = stream;
463
685
  const emit = (kind, payload) => {
464
- queue = queue.then(async () => {
686
+ state.queue = state.queue
687
+ .catch(() => { })
688
+ .then(async () => {
465
689
  const event = {
466
690
  eventId: crypto.randomUUID(),
467
- sequence: sequence + 1,
691
+ sequence: state.sequence + 1,
468
692
  turnId,
469
693
  kind,
470
694
  payload: kind === "PREPARATION_COMPLETED" ? payload : redactPayload(payload, redact),
@@ -477,27 +701,32 @@ function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistS
477
701
  generation: host.generation,
478
702
  events: [event],
479
703
  });
480
- sequence = event.sequence;
481
- await persistSequence?.(event.sequence);
704
+ state.sequence = event.sequence;
705
+ await state.persist?.(event.sequence);
482
706
  return;
483
707
  }
484
708
  catch (error) {
485
709
  if (error instanceof PlatformClientError &&
486
710
  error.status === 409 &&
487
- error.code === "CODING_EVENT_TURN_STALE")
711
+ error.code === "CODING_EVENT_TURN_STALE") {
712
+ if (["EXECUTION_STARTED", "EXECUTION_PAUSED", "CONVERSATION_COMPLETED"].includes(kind))
713
+ throw error;
488
714
  return;
715
+ }
489
716
  process.stderr.write(`[local-coding-host] 事件上报失败:${redact(error instanceof Error ? error.message : String(error))}\n`);
490
717
  if (attempt === EVENT_APPEND_MAX_ATTEMPTS) {
491
718
  process.stderr.write(`[local-coding-host] 事件上报放弃:${event.kind} sequence=${event.sequence} attempts=${attempt}\n`);
719
+ if (["EXECUTION_STARTED", "EXECUTION_PAUSED", "CONVERSATION_COMPLETED"].includes(kind))
720
+ throw error;
492
721
  return;
493
722
  }
494
723
  await new Promise((resolve) => setTimeout(resolve, 0));
495
724
  }
496
725
  }
497
726
  });
498
- return queue;
727
+ return state.queue;
499
728
  };
500
- return Object.assign(emit, { flush: () => queue });
729
+ return Object.assign(emit, { flush: () => state.queue });
501
730
  }
502
731
  function redactPayload(value, redact) {
503
732
  return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactPayloadValue(item, redact)]));
@@ -511,7 +740,7 @@ function redactPayloadValue(value, redact) {
511
740
  return redactPayload(value, redact);
512
741
  return value;
513
742
  }
514
- async function openPiSession(options) {
743
+ export async function openPiSession(options) {
515
744
  await mkdir(options.sessionDir, { recursive: true, mode: 0o700 });
516
745
  await mkdir(options.agentDir, { recursive: true, mode: 0o700 });
517
746
  let sessionManager;
@@ -558,10 +787,49 @@ async function openPiSession(options) {
558
787
  resourceLoader: loader,
559
788
  sessionManager,
560
789
  });
790
+ const savedModel = sessionManager.buildSessionContext().model;
791
+ if (options.model &&
792
+ (savedModel?.provider !== options.model.provider || savedModel?.modelId !== options.model.id)) {
793
+ try {
794
+ // SDK 的显式 model 决定本轮调用,已有会话的模型变更仍通过原生接口落盘。
795
+ await session.setModel(options.model);
796
+ }
797
+ catch (error) {
798
+ session.dispose();
799
+ throw error;
800
+ }
801
+ }
802
+ // SDK 的显式 thinkingLevel 不更新已有会话元数据,补记本轮实际档位。
803
+ if (sessionManager.buildSessionContext().thinkingLevel !== session.thinkingLevel) {
804
+ sessionManager.appendThinkingLevelChange(session.thinkingLevel);
805
+ }
806
+ await session.bindExtensions({ mode: "print" });
561
807
  session.setAutoCompactionEnabled(true);
562
808
  return { session, sessionId };
563
809
  }
564
810
  function subscribeSession(session, emit, observed, redact) {
811
+ const publishContext = (compacting = false) => {
812
+ const model = session.model;
813
+ if (!model)
814
+ return;
815
+ const usage = session.getContextUsage();
816
+ if (!usage)
817
+ return;
818
+ const settings = session.settingsManager.getCompactionSettings(model);
819
+ void emit("CONTEXT_UPDATED", {
820
+ contextUsage: {
821
+ modelId: `${model.provider}/${model.id}`,
822
+ contextWindow: usage.contextWindow,
823
+ tokens: compacting ? null : usage.tokens,
824
+ status: compacting ? "COMPACTING" : usage.tokens === null ? "UNKNOWN" : "READY",
825
+ source: "ESTIMATE",
826
+ compressionThreshold: settings.enabled
827
+ ? Math.max(1, usage.contextWindow - settings.reserveTokens)
828
+ : null,
829
+ },
830
+ });
831
+ };
832
+ publishContext();
565
833
  let messageId = crypto.randomUUID();
566
834
  let pendingText = "";
567
835
  let textFlushTimer;
@@ -588,17 +856,24 @@ function subscribeSession(session, emit, observed, redact) {
588
856
  void emit(update.kind, update.payload);
589
857
  };
590
858
  session.subscribe((event) => {
591
- if (event.type === "message_start" && event.message.role === "assistant") {
859
+ if (event.type === "agent_start") {
860
+ publishContext();
861
+ if (observed.messageQueue)
862
+ void emit("MESSAGE_QUEUE_UPDATED", { mainAgentRunning: true });
863
+ }
864
+ else if (event.type === "agent_settled") {
865
+ publishContext();
866
+ if (observed.messageQueue)
867
+ void emit("MESSAGE_QUEUE_UPDATED", { mainAgentRunning: false });
868
+ }
869
+ else if (event.type === "message_start" && event.message.role === "assistant") {
592
870
  messageId = crypto.randomUUID();
593
871
  void emit("MESSAGE_STARTED", { messageId });
594
872
  }
595
873
  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
- }
874
+ const id = observed.messageQueue?.consumed(event.message);
875
+ if (id)
876
+ void emit("MESSAGE_QUEUE_UPDATED", { messageId: id, deliveryStatus: "READ" });
602
877
  }
603
878
  else if (event.type === "agent_end") {
604
879
  observed.nativeWillRetry = event.willRetry === true;
@@ -619,65 +894,39 @@ function subscribeSession(session, emit, observed, redact) {
619
894
  }
620
895
  else if (event.type === "tool_execution_start") {
621
896
  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
- });
897
+ if (event.toolName === "subagent")
898
+ return;
899
+ void emit("TOOL_STARTED", {
900
+ activityKey: event.toolCallId,
901
+ toolName: event.toolName,
902
+ summary: `执行 ${event.toolName}`,
903
+ });
637
904
  }
638
905
  else if (event.type === "tool_execution_update") {
906
+ if (event.toolName === "subagent")
907
+ return;
639
908
  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
- },
909
+ kind: "TOOL_UPDATED",
910
+ payload: {
911
+ activityKey: event.toolCallId,
912
+ toolName: event.toolName,
913
+ currentAction: redact(textFromResult(event.partialResult)),
914
+ },
652
915
  });
653
916
  if (!toolUpdateTimers.has(event.toolCallId))
654
917
  toolUpdateTimers.set(event.toolCallId, setTimeout(() => flushToolUpdate(event.toolCallId), 200));
655
918
  }
656
919
  else if (event.type === "tool_execution_end") {
657
- flushToolUpdate(event.toolCallId);
658
- const member = observed.activeTasks.get(event.toolCallId);
920
+ publishContext();
659
921
  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
- });
922
+ return;
923
+ flushToolUpdate(event.toolCallId);
924
+ void emit("TOOL_COMPLETED", {
925
+ activityKey: event.toolCallId,
926
+ toolName: event.toolName,
927
+ isError: event.isError === true,
928
+ summary: redact(textFromResult(event.result)),
929
+ });
681
930
  }
682
931
  else if (event.type === "bash_execution_update" && event.delta) {
683
932
  const activityKey = event.id ?? "bash";
@@ -693,6 +942,7 @@ function subscribeSession(session, emit, observed, redact) {
693
942
  toolUpdateTimers.set(activityKey, setTimeout(() => flushToolUpdate(activityKey), 200));
694
943
  }
695
944
  else if (event.type === "compaction_start") {
945
+ publishContext(true);
696
946
  flushText();
697
947
  void emit("TOOL_STARTED", {
698
948
  activityKey: `compaction:${event.reason}`,
@@ -701,6 +951,7 @@ function subscribeSession(session, emit, observed, redact) {
701
951
  });
702
952
  }
703
953
  else if (event.type === "compaction_end") {
954
+ publishContext();
704
955
  void emit("TOOL_COMPLETED", {
705
956
  activityKey: `compaction:${event.reason}`,
706
957
  toolName: "compaction",
@@ -711,6 +962,7 @@ function subscribeSession(session, emit, observed, redact) {
711
962
  });
712
963
  }
713
964
  else if (event.type === "message_end" && event.message.role === "assistant") {
965
+ publishContext();
714
966
  flushText();
715
967
  if ("stopReason" in event.message && event.message.stopReason)
716
968
  observed.lastModelStopReason = String(event.message.stopReason);
@@ -722,16 +974,8 @@ function subscribeSession(session, emit, observed, redact) {
722
974
  contentMarkdown: redact(content),
723
975
  messageId,
724
976
  };
725
- if (usage) {
726
- payload.usage = {
727
- input: usage.input,
728
- output: usage.output,
729
- cacheRead: usage.cacheRead,
730
- cacheWrite: usage.cacheWrite,
731
- totalTokens: usage.totalTokens,
732
- };
733
- }
734
- if (content.trim())
977
+ Object.assign(payload, localModelUsage(event.message, session.model));
978
+ if (content.trim() || usage)
735
979
  void emit("MESSAGE_COMPLETED", payload);
736
980
  }
737
981
  });
@@ -750,260 +994,154 @@ function textFromResult(value) {
750
994
  return "";
751
995
  return textFromMessage(value.content);
752
996
  }
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 });
997
+ async function driveToTerminal(session, initial, observed, client, host, runId, turnId, emit, phase, images = [], completeConversation = async () => { }, continuation) {
998
+ if (observed.stopping)
999
+ return;
1000
+ if (continuation && observed.userIntervened)
1001
+ return;
1002
+ const execution = images.length > 0 ? session.prompt(initial, { images }) : session.prompt(initial);
1003
+ // Observe a synchronous rejection while the invocation fact is persisted.
1004
+ const outcome = execution.then(() => ({ ok: true }), (error) => ({ ok: false, error }));
1005
+ if (continuation) {
1006
+ await emit("EXECUTION_CONTINUED", {
1007
+ phase,
1008
+ ...continuation,
1009
+ prompt: initial,
1010
+ deliveryStatus: "SUBMITTED",
1011
+ });
1012
+ }
1013
+ const result = await outcome;
1014
+ if (!result.ok)
1015
+ throw result.error;
1016
+ await session.waitForIdle();
1017
+ observed.awaitingDecision = true;
1018
+ if (observed.stopping)
1019
+ return;
1020
+ const stopReason = observed.terminalClosed
1021
+ ? "TERMINAL_CLOSED"
1022
+ : (observed.lastModelStopReason ?? "idle");
1023
+ const endFact = { phase, stopReason, nativeStopReason: observed.lastModelStopReason ?? null };
1024
+ if (observed.terminalReached) {
1025
+ await emit("EXECUTION_DECIDED", {
1026
+ ...endFact,
1027
+ decision: "SKIP",
1028
+ reason: observed.deploymentCheckPaused ? "DEPLOYMENT_CHECK_PAUSED" : "TERMINAL_ACCEPTED",
1029
+ model: null,
1030
+ });
1031
+ return;
1032
+ }
1033
+ if (observed.lastModelStopReason === "aborted" ||
1034
+ (observed.lastModelStopReason === "error" && observed.nativeWillRetry !== true)) {
1035
+ const loginRequired = isProviderLoginRequired(observed.lastModelError);
1036
+ const code = loginRequired
1037
+ ? "PROVIDER_LOGIN_REQUIRED"
1038
+ : observed.lastModelError?.split("\n")[0]?.slice(0, 200) ||
1039
+ (observed.lastModelStopReason === "aborted" ? "PI_MODEL_ABORTED" : "MODEL_REQUEST_FAILED");
1040
+ const message = loginRequired
1041
+ ? "模型服务登录已失效,请到本地开发环境重新登录对应订阅后重试。"
1042
+ : (observed.lastModelError ?? code);
1043
+ await emit("EXECUTION_DECIDED", {
1044
+ ...endFact,
1045
+ decision: "SKIP",
1046
+ reason: "MODEL_ERROR",
1047
+ model: null,
1048
+ });
1049
+ if (observed.stopping)
783
1050
  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
1051
  await client.reportCodingTerminal(runId, {
792
1052
  hostId: host.hostId,
793
1053
  generation: host.generation,
794
1054
  turnId,
795
1055
  kind: "FAILED",
796
- summary: observed.lastDeliveryError ?? "Agent 多次结束但没有形成明确终态",
1056
+ summary: message,
797
1057
  knownIssues: "",
798
- failureCode: observed.lastDeliveryError?.split("\n")[0]?.slice(0, 200) ?? "PI_TERMINAL_TOOL_NOT_CALLED",
1058
+ failureCode: code,
799
1059
  projectFacts: [],
800
1060
  testFacts: [],
801
1061
  });
802
1062
  observed.terminalReached = true;
803
- await emit("RUN_FAILED", { code: "PI_TERMINAL_TOOL_NOT_CALLED" });
1063
+ await emit("RUN_FAILED", { phase, code, message });
1064
+ return;
804
1065
  }
805
- }
806
- function terminalTools(client, host, runId, turnId, runRoot, input, observed, redact, persistBatch, pendingBatch, emitQuestion, flushEvents) {
807
- const definitions = [];
808
- const add = (definition) => {
809
- definitions.push(definition);
810
- };
811
- add({
812
- name: input.executionPurpose === "RELEASE"
813
- ? "submit_pull_requests"
814
- : input.phase === "DEVELOPMENT"
815
- ? "complete_development"
816
- : "submit_delivery",
817
- label: "提交交付",
818
- description: input.executionPurpose === "RELEASE"
819
- ? "核实上线清单并提交分支后,使用 Runtime GitLab 身份创建或复用 MR 并登记工单。"
820
- : "读取并校验本阶段最终交付文件,上传成功后完成当前阶段。",
821
- parameters: input.executionPurpose === "RELEASE"
822
- ? Type.Object({
823
- checklistSummary: Type.String({
824
- description: "核实依据、无需修改或修改摘要;缺口未解决时不要提交",
825
- }),
826
- projects: Type.Array(Type.Object({
827
- project: Type.String({
828
- description: `项目名称,只能选择:${input.projects.map((p) => p.name).join("、")}`,
829
- }),
830
- title: Type.String({ description: "包含工单编号及本项目变更摘要的 MR 标题" }),
831
- description: Type.String({
832
- description: "任务目的、本项目改动、测试与验收结论、发布注意、依赖、风险回滚及清单和报告链接",
833
- }),
834
- })),
835
- })
836
- : Type.Object({}),
837
- async execute(_toolCallId, rawParams) {
838
- try {
839
- if (input.executionPurpose === "RELEASE") {
840
- const params = rawParams;
841
- if (!params?.checklistSummary?.trim())
842
- throw new Error("AIP_RELEASE_CHECKLIST_SUMMARY_REQUIRED");
843
- const files = await collectDeliveryFiles(runRoot, input, ["RELEASE_CHECKLIST"]);
844
- const checklist = {
845
- status: "VERIFIED",
846
- summary: params.checklistSummary,
847
- markdown: files[0].content,
848
- };
849
- await client.submitPullRequests(runId, {
850
- hostId: host.hostId,
851
- generation: host.generation,
852
- turnId,
853
- workItemId: input.workItem.id,
854
- projects: [],
855
- checklist,
856
- });
857
- const context = {
858
- runId,
859
- turnId,
860
- workItem: input.workItem,
861
- projects: input.projects,
862
- primaryProjectId: input.primaryProjectId,
863
- };
864
- for (const project of input.projects) {
865
- const explanation = params.projects?.find((item) => item.project === project.name);
866
- if (!explanation)
867
- throw new Error(`AIP_MR_DESCRIPTION_REQUIRED:${project.name}`);
868
- const pendingFile = join(runRoot, `mr-pending-${safeName(project.id)}.json`);
869
- const result = await prepareMergeRequest({
870
- project,
871
- cwd: join(runRoot, safeName(project.name)),
872
- title: explanation.title,
873
- description: explanation.description,
874
- pendingFile,
875
- });
876
- await registerMergeRequest(client, context, host, result, pendingFile);
877
- }
878
- await client.submitPullRequests(runId, {
879
- hostId: host.hostId,
880
- generation: host.generation,
881
- turnId,
882
- workItemId: input.workItem.id,
883
- projects: [],
884
- completeRelease: true,
885
- });
886
- observed.terminalReached = true;
887
- return toolResult("上线清单已核实,MR 已提交并登记,等待合并;平台负责查询最新状态。");
888
- }
889
- const existing = pendingBatch();
890
- if (existing && existing.turnId === turnId) {
891
- await flushEvents();
892
- await uploadBatch(client, runId, existing);
893
- await persistBatch(undefined);
894
- observed.terminalReached = true;
895
- return toolResult("交付已提交,等待经办人确认。");
896
- }
897
- const files = await collectDeliveryFiles(runRoot, input, input.phase === "DEVELOPMENT"
898
- ? Object.keys(DELIVERY_FILES).filter((slot) => STAGE_DELIVERY_PROTOCOLS.DEVELOPMENT.requiredFiles.includes(DELIVERY_FILES[slot]))
899
- : undefined);
900
- const projectFacts = await collectProjectFacts(runRoot, input.projects, input.workItem.key, input.primaryProjectId);
901
- const batch = {
902
- hostId: host.hostId,
903
- generation: host.generation,
904
- turnId,
905
- kind: input.phase === "DEVELOPMENT" ? "DEVELOPMENT_COMPLETED" : "DELIVERY",
906
- submissionId: crypto.randomUUID(),
907
- files,
908
- summary: files.find((file) => file.slot === "SUMMARY").content,
909
- knownIssues: "",
910
- projectFacts,
911
- testFacts: [],
912
- };
913
- await persistBatch(batch);
914
- await flushEvents();
915
- await uploadBatch(client, runId, batch);
916
- await persistBatch(undefined);
917
- observed.terminalReached = true;
918
- return toolResult("交付已提交,等待经办人确认。");
919
- }
920
- catch (error) {
921
- observed.lastDeliveryError = error instanceof Error ? error.message : "AIP_DELIVERY_FAILED";
922
- if (error instanceof PlatformClientError && error.status < 500 && error.status !== 429)
923
- await persistBatch(undefined);
924
- throw error;
925
- }
926
- },
927
- });
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
- if (input.executionPurpose === "RELEASE")
941
- add({
942
- name: "request_code_changes",
943
- label: "返回开发修改",
944
- description: "用户明确要求修改应用代码时,将工单返回开发,再测试和人工确认;仅讨论或文档修改不要调用。",
945
- parameters: Type.Object({
946
- reason: Type.String({ description: "用户要求修改的内容及原因" }),
947
- }),
948
- async execute(_id, params) {
949
- await client.request(`/api/v1/coding-runs/${runId}/return-to-development`, {
950
- method: "POST",
951
- body: JSON.stringify({
952
- hostId: host.hostId,
953
- generation: host.generation,
954
- turnId,
955
- reason: params.reason,
956
- }),
957
- });
958
- observed.terminalReached = true;
959
- return toolResult("已返回开发,等待下一执行接手续改,完成后进入测试与人工确认。");
960
- },
1066
+ // Judge reads durable messages. It queues automatic work through the same command
1067
+ // claim boundary that gives user messages and stop priority.
1068
+ await emit.flush();
1069
+ if (observed.stopping)
1070
+ return;
1071
+ let decision;
1072
+ try {
1073
+ decision = await client.evaluateContinuation(runId, {
1074
+ hostId: host.hostId,
1075
+ generation: host.generation,
1076
+ turnId,
1077
+ stopReason,
1078
+ ...(observed.lastDeliveryError
1079
+ ? { lastDeliveryError: observed.lastDeliveryError.slice(0, 2000) }
1080
+ : {}),
961
1081
  });
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
- return (api) => {
993
- for (const definition of definitions)
994
- api.registerTool({
995
- name: definition.name,
996
- label: definition.label,
997
- description: definition.description,
998
- parameters: definition.parameters,
999
- async execute(id, args, signal) {
1000
- signal?.throwIfAborted();
1001
- return definition.execute(id, args);
1002
- },
1082
+ }
1083
+ catch (error) {
1084
+ if (observed.stopping)
1085
+ return;
1086
+ await emit("EXECUTION_DECIDED", {
1087
+ ...endFact,
1088
+ decision: "SKIP",
1089
+ reason: "JUDGE_REQUEST_FAILED",
1090
+ model: null,
1091
+ explanation: error instanceof Error ? error.message : String(error),
1092
+ });
1093
+ if (observed.handingOffInput)
1094
+ return;
1095
+ await completeConversation();
1096
+ observed.terminalReached = true;
1097
+ return;
1098
+ }
1099
+ if (observed.stopping)
1100
+ return;
1101
+ await emit("EXECUTION_DECIDED", { ...endFact, ...decision });
1102
+ // The platform has queued the continuation; never execute the returned prompt
1103
+ // directly, because a user can cancel it before the host claims the command.
1104
+ if (decision.decision === "CONTINUE")
1105
+ return;
1106
+ if (observed.stopping || observed.handingOffInput)
1107
+ return;
1108
+ await completeConversation();
1109
+ observed.terminalReached = true;
1110
+ }
1111
+ export async function prepareDeploymentProjects(runRoot, projects, configuration) {
1112
+ await mkdir(runRoot, { recursive: true, mode: 0o700 });
1113
+ for (const project of projects) {
1114
+ const target = join(runRoot, safeName(project.name));
1115
+ const branch = configuration?.projects.find((p) => p.projectId === project.id)?.testBranch ??
1116
+ project.testBranch;
1117
+ if (!branch)
1118
+ throw new Error("DEPLOYMENT_TEST_BRANCH_REQUIRED");
1119
+ if (!(await exists(target))) {
1120
+ await execFileAsync("git", [
1121
+ "clone",
1122
+ "--branch",
1123
+ branch,
1124
+ "--",
1125
+ project.repositoryUrl,
1126
+ target,
1127
+ ]).catch(() => {
1128
+ throw new Error("DEPLOYMENT_REPOSITORY_PREPARE_FAILED");
1003
1129
  });
1004
- };
1130
+ }
1131
+ // Reuse the interrupted deployment as-is; the employee resolves its Git state.
1132
+ const [{ stdout: origin }, { stdout: common }] = await Promise.all([
1133
+ execFileAsync("git", ["config", "--get", "remote.origin.url"], { cwd: target }),
1134
+ execFileAsync("git", ["rev-parse", "--git-common-dir"], { cwd: target }),
1135
+ ]);
1136
+ if ((await canonicalRepository(origin.trim())) !==
1137
+ (await canonicalRepository(project.repositoryUrl)))
1138
+ throw new Error("CODING_REPOSITORY_ORIGIN_MISMATCH");
1139
+ if ((await realpath(resolve(target, common.trim()))) !== (await realpath(join(target, ".git"))))
1140
+ throw new Error("DEPLOYMENT_REPOSITORY_NOT_ISOLATED");
1141
+ }
1142
+ return projects.length === 1 ? join(runRoot, safeName(projects[0].name)) : runRoot;
1005
1143
  }
1006
- export async function prepareProjects(cacheRoot, runRoot, runId, projects, resume) {
1144
+ export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects, resume, verifyRemote = false, uninitializedProjectIds = []) {
1007
1145
  await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
1008
1146
  for (const project of projects) {
1009
1147
  const mirror = join(cacheRoot, `${project.id}.git`);
@@ -1023,8 +1161,8 @@ export async function prepareProjects(cacheRoot, runRoot, runId, projects, resum
1023
1161
  (await canonicalRepository(project.repositoryUrl)))
1024
1162
  throw new Error("CODING_REPOSITORY_ORIGIN_MISMATCH");
1025
1163
  }
1026
- if (!resume) {
1027
- // Shared worktrees own refs/heads/aip/*; fetching must only update remote-tracking refs.
1164
+ if (!resume || verifyRemote) {
1165
+ // Never fetch into local branch refs: stopped worktrees may contain unpublished commits.
1028
1166
  await execFileAsync("git", [
1029
1167
  "--git-dir",
1030
1168
  mirror,
@@ -1033,44 +1171,81 @@ export async function prepareProjects(cacheRoot, runRoot, runId, projects, resum
1033
1171
  "remote.origin.fetch",
1034
1172
  "+refs/heads/*:refs/remotes/origin/*",
1035
1173
  ]);
1036
- await execFileAsync("git", ["--git-dir", mirror, "fetch", "origin"]).catch(() => {
1174
+ await execFileAsync("git", ["--git-dir", mirror, "fetch", "--prune", "origin"]).catch(() => {
1037
1175
  throw new Error("CODING_REPOSITORY_ACCESS_FAILED");
1038
1176
  });
1039
- await execFileAsync("git", [
1040
- "--git-dir",
1041
- mirror,
1042
- "cat-file",
1043
- "-e",
1044
- `refs/remotes/origin/${project.baselineBranch}`,
1045
- ]).catch(() => {
1046
- throw new Error("CODING_BASELINE_BRANCH_UNAVAILABLE");
1047
- });
1048
1177
  }
1049
1178
  const target = join(runRoot, safeName(project.name));
1050
- const expectedBranch = project.devBranch || `aip/${runId}/${project.id.slice(0, 8)}`;
1179
+ const expectedBranch = project.devBranch || `aip/${workItemKey}`;
1051
1180
  const existingWorktree = await exists(target);
1052
1181
  if (!existingWorktree) {
1053
1182
  if (resume)
1054
1183
  throw new Error("CODING_RUN_WORKTREE_MISSING");
1055
- await execFileAsync("git", [
1184
+ const remoteExists = await execFileAsync("git", [
1056
1185
  "--git-dir",
1057
1186
  mirror,
1058
- "worktree",
1059
- "add",
1060
- "-b",
1061
- expectedBranch,
1062
- target,
1063
- `refs/remotes/origin/${project.baselineBranch}`,
1064
- ]).catch(() => {
1065
- throw new Error("CODING_WORKTREE_CREATE_FAILED");
1066
- });
1187
+ "show-ref",
1188
+ "--verify",
1189
+ "--quiet",
1190
+ `refs/remotes/origin/${expectedBranch}`,
1191
+ ]).then(() => true, () => false);
1192
+ const localExists = await execFileAsync("git", [
1193
+ "--git-dir",
1194
+ mirror,
1195
+ "show-ref",
1196
+ "--verify",
1197
+ "--quiet",
1198
+ `refs/heads/${expectedBranch}`,
1199
+ ]).then(() => true, () => false);
1200
+ if (remoteExists) {
1201
+ // A new execution owns its branch ref; an old execution keeps its worktree untouched.
1202
+ await execFileAsync("git", [
1203
+ "clone",
1204
+ "--single-branch",
1205
+ "--branch",
1206
+ expectedBranch,
1207
+ project.repositoryUrl,
1208
+ target,
1209
+ ]).catch(() => {
1210
+ throw new Error("CODING_REPOSITORY_ACCESS_FAILED");
1211
+ });
1212
+ }
1213
+ else {
1214
+ if (localExists || (verifyRemote && !uninitializedProjectIds.includes(project.id)))
1215
+ throw new Error(localExists
1216
+ ? "CODING_BRANCH_NOT_PUBLISHED:本机工单分支尚未推送,请检查并同步后再继续。"
1217
+ : "CODING_BRANCH_INITIALIZATION_UNCONFIRMED:远端尚无工单分支,原工作区初始化状态未确认,请检查后再继续。");
1218
+ await execFileAsync("git", [
1219
+ "--git-dir",
1220
+ mirror,
1221
+ "cat-file",
1222
+ "-e",
1223
+ `refs/remotes/origin/${project.baselineBranch}`,
1224
+ ]).catch(() => {
1225
+ throw new Error("CODING_BASELINE_BRANCH_UNAVAILABLE");
1226
+ });
1227
+ await execFileAsync("git", [
1228
+ "--git-dir",
1229
+ mirror,
1230
+ "worktree",
1231
+ "add",
1232
+ "-b",
1233
+ expectedBranch,
1234
+ target,
1235
+ `refs/remotes/origin/${project.baselineBranch}`,
1236
+ ]).catch(() => {
1237
+ throw new Error("CODING_WORKTREE_CREATE_FAILED");
1238
+ });
1239
+ }
1067
1240
  }
1068
- await validateWorktree(mirror, target, expectedBranch, resume || existingWorktree);
1241
+ await validateWorktree(mirror, target, expectedBranch, resume || existingWorktree, project.repositoryUrl);
1242
+ if (existingWorktree && verifyRemote)
1243
+ await synchronizeRemoteWorkspace(target, expectedBranch);
1069
1244
  }
1070
1245
  const onlyProject = projects.length === 1 ? projects[0] : null;
1071
1246
  return onlyProject ? join(runRoot, safeName(onlyProject.name)) : runRoot;
1072
1247
  }
1073
- async function validateWorktree(mirror, target, expectedBranch, resume) {
1248
+ async function validateWorktree(mirror, target, expectedBranch, resume, repositoryUrl) {
1074
1249
  const facts = await Promise.all([
1075
1250
  execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: target }),
1076
1251
  execFileAsync("git", ["rev-parse", "--git-common-dir"], { cwd: target }),
@@ -1086,8 +1261,15 @@ async function validateWorktree(mirror, target, expectedBranch, resume) {
1086
1261
  realpath(commonPath.startsWith("/") ? commonPath : resolve(target, commonPath)),
1087
1262
  realpath(mirror),
1088
1263
  ]);
1089
- if (actualCommon !== expectedCommon)
1090
- throw new Error("CODING_WORKTREE_OWNERSHIP_INVALID");
1264
+ if (actualCommon !== expectedCommon) {
1265
+ if (actualCommon !== (await realpath(join(target, ".git"))))
1266
+ throw new Error("CODING_WORKTREE_OWNERSHIP_INVALID");
1267
+ const { stdout: origin } = await execFileAsync("git", ["remote", "get-url", "origin"], {
1268
+ cwd: target,
1269
+ });
1270
+ if ((await canonicalRepository(origin.trim())) !== (await canonicalRepository(repositoryUrl)))
1271
+ throw new Error("CODING_REPOSITORY_ORIGIN_MISMATCH");
1272
+ }
1091
1273
  if (branch.trim() !== expectedBranch)
1092
1274
  throw new Error("CODING_WORKTREE_BRANCH_MISMATCH");
1093
1275
  if (resume)
@@ -1107,40 +1289,50 @@ async function canonicalRepository(value) {
1107
1289
  }
1108
1290
  return value.replace(/\/$/u, "");
1109
1291
  }
1110
- export function initialPrompt(input, runRoot, responsible = true) {
1111
- const directory = deliveryDirectory(runRoot, input);
1112
- 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。平台查询状态,不负责创建。仅说明或更新文档不退回研发;不执行实际发布、知识整理、关单。`;
1292
+ function executionInstructions(input, runRoot) {
1293
+ // 环境事实与授权单独呈现,业务方法由平台 nodeRules 提供。
1294
+ return `本轮执行上下文:\n${JSON.stringify({
1295
+ phase: input.phase,
1296
+ executionPurpose: input.executionPurpose ?? null,
1297
+ discussionOnly: input.conversationContext?.discussionOnly ?? false,
1298
+ deploymentConfiguration: input.deploymentConfiguration ?? null,
1299
+ testHandoff: input.testHandoff
1300
+ ? {
1301
+ summary: input.testHandoff.summary,
1302
+ ...(input.testHandoff.files
1303
+ ? {
1304
+ files: input.testHandoff.files.map(({ source, baseSource, materialPath, baseMaterialPath, ...metadata }) => ({
1305
+ ...metadata,
1306
+ materialPath: join(deliveryDirectory(runRoot, input), materialPath),
1307
+ ...(baseMaterialPath
1308
+ ? {
1309
+ baseMaterialPath: join(deliveryDirectory(runRoot, input), baseMaterialPath),
1310
+ }
1311
+ : {}),
1312
+ })),
1313
+ }
1314
+ : {}),
1315
+ }
1316
+ : null,
1317
+ }, null, 2)}`;
1318
+ }
1319
+ async function workspaceFingerprint(runRoot, input) {
1320
+ return JSON.stringify(await Promise.all(input.projects.map(async (project) => {
1321
+ const cwd = join(runRoot, safeName(project.name));
1322
+ const [head, branch, tree] = await Promise.all([
1323
+ execFileAsync("git", ["rev-parse", "HEAD"], { cwd }),
1324
+ execFileAsync("git", ["symbolic-ref", "--short", "-q", "HEAD"], { cwd }),
1325
+ worktreeTreeHash(cwd),
1326
+ ]);
1327
+ return [project.id, head.stdout.trim(), branch.stdout.trim(), tree];
1328
+ })));
1329
+ }
1330
+ export function initialPrompt(input, runRoot, _responsible = true, materialRoot = runRoot) {
1331
+ const directory = deliveryDirectory(materialRoot, input);
1114
1332
  const projects = input.projects
1115
1333
  .map((p) => `- ${p.name}: ${join(runRoot, safeName(p.name))}(开发分支 ${p.devBranch} → 基准分支 ${p.baselineBranch};测试分支 ${p.testBranch})${p.id === input.primaryProjectId ? "(主项目)" : ""}`)
1116
1334
  .join("\n");
1117
- const phase = input.phase === "DEVELOPMENT" ? "DEVELOPMENT" : "INTEGRATION";
1118
- const protocol = STAGE_DELIVERY_PROTOCOLS[phase];
1119
- const delivery = input.stageDeliverySnapshots?.[phase];
1120
- if (responsible && !delivery)
1121
- 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;本地副本未被覆盖,对照文件不收集。` : ""}`;
1123
- if (!responsible)
1124
- 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}。`;
1126
- }
1127
- async function uploadBatch(client, runId, batch) {
1128
- for (let attempt = 0;; attempt++) {
1129
- try {
1130
- await client.reportCodingTerminal(runId, batch);
1131
- return;
1132
- }
1133
- catch (error) {
1134
- if (error instanceof PlatformClientError && error.status < 500 && error.status !== 429)
1135
- throw error;
1136
- if (attempt >= 2)
1137
- throw new Error(`AIP_DELIVERY_UPLOAD_FAILED:同批材料尚未提交成功,可恢复重试;${error instanceof Error ? error.message : "网络错误"}`);
1138
- await new Promise((resolve) => setTimeout(resolve, 300 * (attempt + 1)));
1139
- }
1140
- }
1141
- }
1142
- function toolResult(text) {
1143
- return { content: [{ type: "text", text }], details: {} };
1335
+ return `工单:${input.workItem.key} ${input.workItem.title}。\n当前阶段:${input.executionPurpose === "RELEASE" ? "RELEASE" : input.phase}。\n项目:\n${projects}\n输入材料:${input.materialPreparation.inputPaths.map((path) => join(directory, path)).join("、")}。\n交付目录:${directory}。${input.inputConflict ? "正式输入材料已更新。" : ""}\n${executionInstructions(input, materialRoot)}`;
1144
1336
  }
1145
1337
  function safeName(value) {
1146
1338
  return basename(value).replace(/[^a-zA-Z0-9._-]/gu, "-") || "project";
@@ -1154,64 +1346,9 @@ async function exists(path) {
1154
1346
  return false;
1155
1347
  }
1156
1348
  }
1157
- async function collectProjectFacts(runRoot, projects, key, primaryProjectId) {
1158
- return Promise.all(projects.map(async (project) => {
1159
- const cwd = join(runRoot, safeName(project.name));
1160
- const excludeKey = project.id === primaryProjectId ? key : undefined;
1161
- const [{ stdout: head }, tree, { stdout: branch }] = await Promise.all([
1162
- execFileAsync("git", ["rev-parse", "HEAD"], { cwd }),
1163
- worktreeTreeHash(cwd, excludeKey),
1164
- execFileAsync("git", ["branch", "--show-current"], { cwd }),
1165
- ]);
1166
- const commitSha = head.trim();
1167
- return {
1168
- projectId: project.id,
1169
- projectName: project.name,
1170
- commitSha,
1171
- treeHash: tree,
1172
- branch: branch.trim(),
1173
- };
1174
- }));
1175
- }
1176
1349
  async function captureProjectRevisions(runRoot, projects, key, primaryProjectId) {
1177
1350
  return collectProjectFacts(runRoot, projects, key, primaryProjectId);
1178
1351
  }
1179
- export async function worktreeTreeHash(cwd, key, committed = false, ref = "HEAD") {
1180
- const temporary = await mkdtemp(join(tmpdir(), "aip-tested-tree-"));
1181
- const index = join(temporary, "index");
1182
- const options = { cwd, env: { ...process.env, GIT_INDEX_FILE: index } };
1183
- try {
1184
- await execFileAsync("git", ["read-tree", ref], options);
1185
- if (!committed)
1186
- await execFileAsync("git", ["add", "-A"], options);
1187
- if (key) {
1188
- const names = [
1189
- "summary.md",
1190
- "prd.md",
1191
- "design.md",
1192
- "plan.md",
1193
- "test_plan.md",
1194
- "report.md",
1195
- "code_review.md",
1196
- "release_checklist.md",
1197
- "prd.platform.md",
1198
- "design.platform.md",
1199
- ];
1200
- await execFileAsync("git", [
1201
- "rm",
1202
- "--cached",
1203
- "--force", // This removes entries only from the dedicated temporary index.
1204
- "--ignore-unmatch",
1205
- "--",
1206
- ...names.map((name) => `docs/aip/${key}/${name}`),
1207
- ], options);
1208
- }
1209
- return (await execFileAsync("git", ["write-tree"], options)).stdout.trim();
1210
- }
1211
- finally {
1212
- await rm(temporary, { recursive: true, force: true });
1213
- }
1214
- }
1215
1352
  export function redactHostText(value, roots = []) {
1216
1353
  let redacted = value;
1217
1354
  for (const root of [...roots].filter(Boolean).sort((a, b) => b.length - a.length))
@@ -1227,4 +1364,84 @@ export function redactHostText(value, roots = []) {
1227
1364
  .replace(/\b[A-Za-z]:\\(?:[^\s'"`<>]+\\?)+/gu, "[本地路径]");
1228
1365
  return redacted.slice(0, 100_000);
1229
1366
  }
1367
+ /** 检查失败命令自己的已知工作区,不从空准备结果推测初始化状态。 */
1368
+ export async function inspectWorkspaceInitialization(runId, input) {
1369
+ const local = paths();
1370
+ return Promise.all(input.projects.map(async (project) => {
1371
+ let status = "UNKNOWN";
1372
+ try {
1373
+ const target = join(local.runWorktrees, runId, safeName(project.name));
1374
+ const mirror = join(local.repositoryCache, `${project.id}.git`);
1375
+ const present = async (path) => {
1376
+ try {
1377
+ await stat(path);
1378
+ return true;
1379
+ }
1380
+ catch (error) {
1381
+ if (error.code === "ENOENT")
1382
+ return false;
1383
+ throw error;
1384
+ }
1385
+ };
1386
+ if (await present(target)) {
1387
+ const branch = (await execFileAsync("git", ["branch", "--show-current"], { cwd: target })).stdout.trim();
1388
+ await execFileAsync("git", ["rev-parse", "--verify", "HEAD^{commit}"], { cwd: target });
1389
+ if (branch === (project.devBranch || `aip/${input.workItem.key}`))
1390
+ status = "INITIALIZED";
1391
+ }
1392
+ else if (!(await present(mirror)))
1393
+ status = "NOT_INITIALIZED";
1394
+ else {
1395
+ const refs = await execFileAsync("git", [
1396
+ "--git-dir",
1397
+ mirror,
1398
+ "for-each-ref",
1399
+ "--format=%(refname)",
1400
+ `refs/heads/${project.devBranch || `aip/${input.workItem.key}`}`,
1401
+ ]);
1402
+ status = refs.stdout.trim() ? "INITIALIZED" : "NOT_INITIALIZED";
1403
+ }
1404
+ }
1405
+ catch {
1406
+ /* 不可访问或损坏的缓存不能证明没有旧成果。 */
1407
+ }
1408
+ return { projectId: project.id, status };
1409
+ }));
1410
+ }
1411
+ export async function inspectPriorTransferWorkspaces(root, runIds, projects) {
1412
+ const notes = [];
1413
+ for (const id of runIds) {
1414
+ if (!/^[0-9a-f-]{36}$/iu.test(id))
1415
+ throw new Error("CODING_PRIOR_RUN_ID_INVALID");
1416
+ for (const project of projects) {
1417
+ const cwd = join(root, id, safeName(project.name));
1418
+ try {
1419
+ try {
1420
+ await stat(cwd);
1421
+ }
1422
+ catch (error) {
1423
+ if (error.code === "ENOENT")
1424
+ continue;
1425
+ throw error;
1426
+ }
1427
+ const origin = (await execFileAsync("git", ["config", "--get", "remote.origin.url"], { cwd })).stdout.trim();
1428
+ const branch = (await execFileAsync("git", ["branch", "--show-current"], { cwd })).stdout.trim();
1429
+ if ((await canonicalRepository(origin)) !==
1430
+ (await canonicalRepository(project.repositoryUrl)) ||
1431
+ branch !== project.devBranch)
1432
+ throw new Error("WORKSPACE_IDENTITY_MISMATCH");
1433
+ await execFileAsync("git", ["fetch", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`], { cwd });
1434
+ const counts = (await execFileAsync("git", ["rev-list", "--left-right", "--count", `HEAD...refs/remotes/origin/${branch}`], { cwd })).stdout
1435
+ .trim()
1436
+ .split(/\s+/u);
1437
+ const dirty = Boolean((await execFileAsync("git", ["status", "--porcelain"], { cwd })).stdout.trim());
1438
+ notes.push(`${project.name},旧执行 ${id}:${dirty ? "有未提交修改" : "无未提交修改"},本地领先 ${counts[0]}、远端领先 ${counts[1]}${Number(counts[0]) && Number(counts[1]) ? "(已分叉)" : ""}。旧现场保留在 ${cwd};本次使用独立工作区,请核对需要保留的修改。`);
1439
+ }
1440
+ catch {
1441
+ notes.push(`${project.name},旧执行 ${id}:无法确认旧现场与远端差异,旧现场未改动,请检查 ${cwd}。`);
1442
+ }
1443
+ }
1444
+ }
1445
+ return notes;
1446
+ }
1230
1447
  //# sourceMappingURL=coding-run-manager.js.map