@bf-meishijian/aip 0.0.6 → 0.0.8

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 (147) hide show
  1. package/README.md +118 -1
  2. package/dist/agent-harness/index.d.ts +4 -0
  3. package/dist/agent-harness/index.d.ts.map +1 -0
  4. package/dist/agent-harness/index.js +4 -0
  5. package/dist/agent-harness/index.js.map +1 -0
  6. package/dist/agent-harness/pi-chat-model.d.ts +35 -0
  7. package/dist/agent-harness/pi-chat-model.d.ts.map +1 -0
  8. package/dist/agent-harness/pi-chat-model.js +285 -0
  9. package/dist/agent-harness/pi-chat-model.js.map +1 -0
  10. package/dist/cli.d.ts +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +60 -23
  13. package/dist/cli.js.map +1 -1
  14. package/dist/coding-run-manager.d.ts +76 -16
  15. package/dist/coding-run-manager.d.ts.map +1 -1
  16. package/dist/coding-run-manager.js +550 -439
  17. package/dist/coding-run-manager.js.map +1 -1
  18. package/dist/coding-subagent-child.d.ts +1 -1
  19. package/dist/coding-subagent-child.d.ts.map +1 -1
  20. package/dist/coding-subagent-child.js +14 -21
  21. package/dist/coding-subagent-child.js.map +1 -1
  22. package/dist/coding-subagents.d.ts +2 -0
  23. package/dist/coding-subagents.d.ts.map +1 -1
  24. package/dist/coding-subagents.js +103 -119
  25. package/dist/coding-subagents.js.map +1 -1
  26. package/dist/daemon.d.ts +4 -1
  27. package/dist/daemon.d.ts.map +1 -1
  28. package/dist/daemon.js +155 -43
  29. package/dist/daemon.js.map +1 -1
  30. package/dist/delivery-files.d.ts +15 -12
  31. package/dist/delivery-files.d.ts.map +1 -1
  32. package/dist/delivery-files.js +124 -78
  33. package/dist/delivery-files.js.map +1 -1
  34. package/dist/delivery-tools.d.ts +28 -0
  35. package/dist/delivery-tools.d.ts.map +1 -0
  36. package/dist/delivery-tools.js +167 -0
  37. package/dist/delivery-tools.js.map +1 -0
  38. package/dist/diagnostics.d.ts +1 -0
  39. package/dist/diagnostics.d.ts.map +1 -1
  40. package/dist/diagnostics.js +48 -14
  41. package/dist/diagnostics.js.map +1 -1
  42. package/dist/host-update.d.ts +3 -0
  43. package/dist/host-update.d.ts.map +1 -0
  44. package/dist/host-update.js +70 -0
  45. package/dist/host-update.js.map +1 -0
  46. package/dist/installer.d.ts +5 -8
  47. package/dist/installer.d.ts.map +1 -1
  48. package/dist/installer.js +155 -65
  49. package/dist/installer.js.map +1 -1
  50. package/dist/linux-service.d.ts +14 -0
  51. package/dist/linux-service.d.ts.map +1 -0
  52. package/dist/linux-service.js +63 -0
  53. package/dist/linux-service.js.map +1 -0
  54. package/dist/live-employee.d.ts +4 -3
  55. package/dist/live-employee.d.ts.map +1 -1
  56. package/dist/live-employee.js +222 -60
  57. package/dist/live-employee.js.map +1 -1
  58. package/dist/local-handoff-workspace.d.ts +20 -0
  59. package/dist/local-handoff-workspace.d.ts.map +1 -0
  60. package/dist/local-handoff-workspace.js +80 -0
  61. package/dist/local-handoff-workspace.js.map +1 -0
  62. package/dist/local-model-usage.d.ts +6 -0
  63. package/dist/local-model-usage.d.ts.map +1 -0
  64. package/dist/local-model-usage.js +40 -0
  65. package/dist/local-model-usage.js.map +1 -0
  66. package/dist/login-url.d.ts +3 -0
  67. package/dist/login-url.d.ts.map +1 -0
  68. package/dist/login-url.js +30 -0
  69. package/dist/login-url.js.map +1 -0
  70. package/dist/paths.d.ts +9 -0
  71. package/dist/paths.d.ts.map +1 -1
  72. package/dist/paths.js +27 -3
  73. package/dist/paths.js.map +1 -1
  74. package/dist/pi-model-service.d.ts +11 -0
  75. package/dist/pi-model-service.d.ts.map +1 -1
  76. package/dist/pi-model-service.js +28 -11
  77. package/dist/pi-model-service.js.map +1 -1
  78. package/dist/platform-client.d.ts +214 -6
  79. package/dist/platform-client.d.ts.map +1 -1
  80. package/dist/platform-client.js +66 -0
  81. package/dist/platform-client.js.map +1 -1
  82. package/dist/profile.d.ts +9 -0
  83. package/dist/profile.d.ts.map +1 -0
  84. package/dist/profile.js +43 -0
  85. package/dist/profile.js.map +1 -0
  86. package/dist/project-facts.d.ts +19 -0
  87. package/dist/project-facts.d.ts.map +1 -0
  88. package/dist/project-facts.js +91 -0
  89. package/dist/project-facts.js.map +1 -0
  90. package/dist/provider-auth-error.d.ts +3 -0
  91. package/dist/provider-auth-error.d.ts.map +1 -0
  92. package/dist/provider-auth-error.js +12 -0
  93. package/dist/provider-auth-error.js.map +1 -0
  94. package/dist/qa-contract.d.ts +2 -0
  95. package/dist/qa-contract.d.ts.map +1 -0
  96. package/dist/qa-contract.js +165 -0
  97. package/dist/qa-contract.js.map +1 -0
  98. package/dist/qa-http-validation.js +145 -0
  99. package/dist/qa-project-checkout.d.ts +12 -0
  100. package/dist/qa-project-checkout.d.ts.map +1 -0
  101. package/dist/qa-project-checkout.js +68 -0
  102. package/dist/qa-project-checkout.js.map +1 -0
  103. package/dist/qa-session-manager.d.ts +101 -0
  104. package/dist/qa-session-manager.d.ts.map +1 -0
  105. package/dist/qa-session-manager.js +581 -0
  106. package/dist/qa-session-manager.js.map +1 -0
  107. package/dist/review-report.d.ts +3 -4
  108. package/dist/review-report.d.ts.map +1 -1
  109. package/dist/review-report.js +14 -12
  110. package/dist/review-report.js.map +1 -1
  111. package/dist/secure-json.d.ts.map +1 -1
  112. package/dist/secure-json.js +64 -2
  113. package/dist/secure-json.js.map +1 -1
  114. package/dist/stage-delivery.js +42 -25
  115. package/dist/state.d.ts +2 -2
  116. package/dist/state.d.ts.map +1 -1
  117. package/dist/state.js +6 -4
  118. package/dist/state.js.map +1 -1
  119. package/dist/test-cli.d.ts +2 -0
  120. package/dist/test-cli.d.ts.map +1 -0
  121. package/dist/test-cli.js +50 -0
  122. package/dist/test-cli.js.map +1 -0
  123. package/dist/test-executor.d.ts +44 -0
  124. package/dist/test-executor.d.ts.map +1 -0
  125. package/dist/test-executor.js +474 -0
  126. package/dist/test-executor.js.map +1 -0
  127. package/dist/test-reports.d.ts +13 -0
  128. package/dist/test-reports.d.ts.map +1 -0
  129. package/dist/test-reports.js +129 -0
  130. package/dist/test-reports.js.map +1 -0
  131. package/dist/test-run-manager.d.ts +24 -0
  132. package/dist/test-run-manager.d.ts.map +1 -0
  133. package/dist/test-run-manager.js +121 -0
  134. package/dist/test-run-manager.js.map +1 -0
  135. package/dist/tool-dispatch.d.ts +14 -0
  136. package/dist/tool-dispatch.d.ts.map +1 -0
  137. package/dist/tool-dispatch.js +56 -0
  138. package/dist/tool-dispatch.js.map +1 -0
  139. package/dist/windows-runtime.d.ts +9 -0
  140. package/dist/windows-runtime.d.ts.map +1 -0
  141. package/dist/windows-runtime.js +64 -0
  142. package/dist/windows-runtime.js.map +1 -0
  143. package/dist/windows-service.d.ts +20 -0
  144. package/dist/windows-service.d.ts.map +1 -0
  145. package/dist/windows-service.js +96 -0
  146. package/dist/windows-service.js.map +1 -0
  147. package/package.json +17 -10
@@ -1,21 +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";
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";
9
13
  import { createCodingSubagents } from "./coding-subagents.js";
10
- import { collectDeliveryFiles, deliveryDirectory, initializeDeliveryFiles, publishReviewMaterials, } from "./delivery-files.js";
14
+ import { commandMaterialReader, deliveryDirectory, initializeDeliveryFiles, publishReviewMaterials, } from "./delivery-files.js";
11
15
  import { liveEmployee } from "./live-employee.js";
12
- import { prepareMergeRequest, registerMergeRequest, } from "./merge-requests.js";
13
16
  import { paths } from "./paths.js";
14
17
  import { PiMessageQueue } from "./pi-message-queue.js";
15
18
  import { PlatformClientError } from "./platform-client.js";
16
19
  import { materializeReviewReport } from "./review-report.js";
17
20
  import { readJson, writeSecureJson } from "./secure-json.js";
18
- import { DELIVERY_FILES, STAGE_DELIVERY_PROTOCOLS, stageEmployeeIds } from "./stage-delivery.js";
21
+ import { stageEmployeeIds } from "./stage-delivery.js";
19
22
  const execFileAsync = promisify(execFile);
20
23
  const EVENT_APPEND_MAX_ATTEMPTS = 8;
21
24
  export class CodingRunManager {
@@ -28,13 +31,49 @@ export class CodingRunManager {
28
31
  return this.active.size > 0;
29
32
  }
30
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
+ }));
31
39
  if (!command.run)
32
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
+ }
33
61
  if (command.run.action === "REVIEW_REPORT") {
34
62
  const report = command.run.reviewReport;
35
63
  if (!report || !command.run.runInput)
36
64
  throw new Error("REVIEW_REPORT_INPUT_MISSING");
37
- const files = await materializeReviewReport(join(paths().runWorktrees, command.run.runId), command.run.runInput, report.files);
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);
38
77
  await client.acknowledgeReviewReport(command.run.runId, {
39
78
  hostId: host.hostId,
40
79
  generation: host.generation,
@@ -71,9 +110,19 @@ export class CodingRunManager {
71
110
  }
72
111
  throw error;
73
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
+ }
74
123
  if (command.run.action === "START") {
75
124
  const input = command.run.runInput;
76
- 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);
77
126
  }
78
127
  else if (command.run.action === "CONTINUE") {
79
128
  const active = this.active.get(command.run.runId);
@@ -84,7 +133,7 @@ export class CodingRunManager {
84
133
  }
85
134
  if (!command.run.runInput || !command.run.contentMarkdown || !command.run.continuation)
86
135
  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);
136
+ await this.start(readMaterial, client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, [], command.run.continuation);
88
137
  }
89
138
  else if (command.run.action === "STEER" || command.run.action === "FOLLOW_UP") {
90
139
  let active = this.active.get(command.run.runId);
@@ -108,6 +157,8 @@ export class CodingRunManager {
108
157
  active = undefined;
109
158
  }
110
159
  if (active) {
160
+ if (command.run.runInput)
161
+ active.input.handoff = command.run.runInput.handoff ?? null;
111
162
  if (images.length > 0 && !active.supportsImages)
112
163
  throw new Error("MODEL_IMAGES_UNSUPPORTED");
113
164
  active.observed.userIntervened = true;
@@ -127,7 +178,7 @@ export class CodingRunManager {
127
178
  await active.finished;
128
179
  if (!command.run.runInput)
129
180
  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);
181
+ await this.start(readMaterial, client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
131
182
  if (command.run.messageId)
132
183
  await this.active.get(command.run.runId)?.emit("MESSAGE_QUEUE_UPDATED", {
133
184
  messageId: command.run.messageId,
@@ -146,7 +197,7 @@ export class CodingRunManager {
146
197
  else {
147
198
  if (!command.run.runInput)
148
199
  throw new Error("CODING_RUN_INPUT_MISSING");
149
- 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);
150
201
  if (command.run.messageId)
151
202
  await this.active.get(command.run.runId)?.emit("MESSAGE_QUEUE_UPDATED", {
152
203
  messageId: command.run.messageId,
@@ -176,7 +227,14 @@ export class CodingRunManager {
176
227
  reason: "STOP_REQUESTED",
177
228
  stopReason: "USER_STOPPED",
178
229
  });
179
- await active.emit("RUN_STOPPED", { phase: active.input.phase, reason: "USER_STOPPED" });
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
+ });
180
238
  }
181
239
  else {
182
240
  const stateFile = join(paths().runState, `${command.run.runId}.json`);
@@ -185,11 +243,17 @@ export class CodingRunManager {
185
243
  phase: state?.phase,
186
244
  reason: "USER_STOPPED",
187
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
+ : [],
188
252
  });
189
253
  }
190
254
  }
191
255
  else if (command.run.action === "RESUME") {
192
- 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);
193
257
  }
194
258
  if (command.run.messageId &&
195
259
  ["START", "RESUME", "STEER", "FOLLOW_UP"].includes(command.run.action)) {
@@ -204,23 +268,39 @@ export class CodingRunManager {
204
268
  });
205
269
  }
206
270
  }
207
- async start(client, host, runId, turnId, input, resume, resumePrompt, images = [], continuation) {
208
- 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);
209
285
  if (active) {
210
286
  if (active.turnId === turnId)
211
287
  return;
212
288
  if (!active.observed.terminalReached && !active.stopping)
213
289
  throw new Error("CODING_RUN_TURN_MISMATCH");
214
290
  await active.finished;
215
- if (this.active.get(runId) === active)
216
- this.active.delete(runId);
291
+ if (this.active.get(activeKey) === active)
292
+ this.active.delete(activeKey);
217
293
  }
218
- if (process.env.AIP_TEST_PI === "1") {
294
+ if (process.env.AIP_TEST_PI === "1" && !input.discussionTurn) {
219
295
  await this.synthetic(client, host, runId, turnId, input);
220
296
  return;
221
297
  }
222
298
  const local = paths();
223
- 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;
224
304
  await mkdir(runRoot, { recursive: true, mode: 0o700 });
225
305
  const stateFile = join(local.runState, `${runId}.json`);
226
306
  const storedState = await readJson(stateFile);
@@ -232,7 +312,27 @@ export class CodingRunManager {
232
312
  project.devBranch ||=
233
313
  storedState?.mrContext?.projects.find((p) => p.id === project.id)?.devBranch ||
234
314
  `aip/${input.workItem.key}`;
235
- const cwd = await prepareProjects(local.repositoryCache, runRoot, input.workItem.key, input.projects, Boolean(storedState?.prepared));
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
+ }
236
336
  const preparationBaselines = storedState?.preparationBaselines ?? {};
237
337
  if (input.phase === "PREPARATION" && !storedState?.prepared) {
238
338
  for (const project of input.projects)
@@ -241,17 +341,23 @@ export class CodingRunManager {
241
341
  })).stdout.trim();
242
342
  await writeSecureJson(stateFile, { ...storedState, preparationBaselines });
243
343
  }
244
- if (!storedState?.prepared || input.inputConflict)
245
- await initializeDeliveryFiles(runRoot, input, Boolean(storedState?.prepared));
344
+ await initializeDeliveryFiles(runRoot, input, Boolean(storedState?.prepared), readMaterial);
246
345
  const deliveryInputHash = createHash("sha256")
247
346
  .update(JSON.stringify([
248
347
  input.primaryProjectId,
249
- input.requirement.body_markdown,
250
- input.technicalDesign?.content,
348
+ input.requirement.source.contentHash,
349
+ input.technicalDesign?.source.contentHash,
251
350
  ]))
252
351
  .digest("hex");
352
+ const newDeploymentRequest = deploymentSession &&
353
+ Boolean(input.deploymentRequestId) &&
354
+ input.deploymentRequestId !== storedState?.deploymentRequestId;
355
+ const deploymentRequestId = deploymentSession
356
+ ? input.deploymentRequestId
357
+ : storedState?.deploymentRequestId;
253
358
  const delegations = storedState?.delegations ?? {};
254
- let pendingDelivery = storedState?.phase !== input.phase ||
359
+ let pendingDelivery = newDeploymentRequest ||
360
+ storedState?.phase !== input.phase ||
255
361
  resumePrompt ||
256
362
  images.length > 0 ||
257
363
  (storedState?.deliveryInputHash && storedState.deliveryInputHash !== deliveryInputHash)
@@ -266,7 +372,7 @@ export class CodingRunManager {
266
372
  : undefined;
267
373
  const phaseSessions = storedState?.phaseSessions ?? {};
268
374
  const sessionPhase = input.executionPurpose === "RELEASE" ? "DEVELOPMENT" : input.phase;
269
- let sessionRef = phaseSessions[sessionPhase];
375
+ let sessionRef = newDeploymentRequest ? undefined : phaseSessions[sessionPhase];
270
376
  let lastEventSequence = storedState?.lastEventSequence ?? 0;
271
377
  const observed = {
272
378
  userIntervened: input.autoContinueEnabled === false,
@@ -274,33 +380,47 @@ export class CodingRunManager {
274
380
  stopping: false,
275
381
  terminalReached: false,
276
382
  };
277
- const persistState = () => writeSecureJson(stateFile, {
278
- phase: input.phase,
279
- mrContext: {
280
- runId,
281
- turnId,
282
- workItem: input.workItem,
283
- projects: input.projects,
284
- primaryProjectId: input.primaryProjectId,
285
- },
286
- prepared: true,
287
- preparationBaselines,
288
- phaseSessions,
289
- sessionRef,
290
- harness: "pi",
291
- lastEventSequence,
292
- delegations,
293
- pendingDelivery,
294
- deliveryInputHash,
295
- });
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
+ });
296
406
  const redact = (value) => redactHostText(value, [runRoot, local.root]);
297
- const emit = eventEmitter(client, host, runId, turnId, lastEventSequence, (sequence) => {
298
- lastEventSequence = sequence;
299
- return persistState();
300
- }, 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);
301
423
  if (input.phase === "PREPARATION") {
302
- if (!storedState?.prepared)
303
- await publishReviewMaterials(runRoot, input);
304
424
  await persistState();
305
425
  await emit("PREPARATION_COMPLETED", {
306
426
  primaryProjectId: input.primaryProjectId,
@@ -316,10 +436,20 @@ export class CodingRunManager {
316
436
  });
317
437
  return;
318
438
  }
319
- const terminal = terminalTools(client, host, runId, turnId, runRoot, input, observed, 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) => {
320
450
  pendingDelivery = batch;
321
451
  await persistState();
322
- }, () => pendingDelivery, () => emit.flush());
452
+ }, () => pendingDelivery, () => emit.flush(), initialEmployeeConfig.toolDefinitions);
323
453
  if (!this.pi.resolveRuntimeModel)
324
454
  throw new Error("MODEL_RUNTIME_UNAVAILABLE");
325
455
  const resolved = await this.pi.resolveRuntimeModel(input.providerId, input.modelId, {
@@ -330,20 +460,15 @@ export class CodingRunManager {
330
460
  });
331
461
  if (images.length > 0 && !resolved.model.input?.includes("image"))
332
462
  throw new Error("MODEL_IMAGES_UNSUPPORTED");
333
- const binding = {
334
- hostId: host.hostId,
335
- generation: host.generation,
336
- turnId,
337
- member: "supervisor",
338
- };
339
463
  const availableAgents = stageEmployeeIds(input);
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, () => {
464
+ const live = liveEmployee(client, runId, binding, initialEmployeeConfig, runRoot, Boolean(input.team), input.executionPurpose === "RELEASE", input.phase, () => observed.terminalReached, () => executionInstructions(input, runRoot), () => {
341
465
  observed.terminalClosed = true;
342
466
  });
343
467
  const delegation = input.executionPurpose === "RELEASE"
344
468
  ? undefined
345
469
  : await createCodingSubagents({
346
470
  client,
471
+ toolDefinitions: initialEmployeeConfig.toolDefinitions,
347
472
  config: {
348
473
  runId,
349
474
  turnId,
@@ -404,6 +529,7 @@ export class CodingRunManager {
404
529
  });
405
530
  }, observed, redact);
406
531
  const activeRun = {
532
+ runRoot,
407
533
  session,
408
534
  supportsImages: resolved.model.input?.includes("image") === true,
409
535
  input,
@@ -413,41 +539,79 @@ export class CodingRunManager {
413
539
  observed,
414
540
  ...(delegation ? { subagents: delegation } : {}),
415
541
  };
416
- this.active.set(runId, activeRun);
417
- phaseSessions[sessionPhase] = sessionRef;
418
- await persistState();
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
- });
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继续当前会话未完成的工作,先读取最新文件和交接结果。" : ""}`;
428
584
  }
429
- if (!continuation && (resumePrompt || images.length > 0)) {
430
- await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
431
- await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
585
+ catch (error) {
586
+ await disposeRun();
587
+ throw error;
432
588
  }
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)
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))
442
605
  .catch(async (error) => {
443
606
  if (activeRun.stopping)
444
607
  return;
445
- await emit("EXECUTION_DECIDED", {
446
- phase: input.phase,
447
- decision: "SKIP",
448
- reason: "MODEL_ERROR",
449
- stopReason: "error",
450
- });
608
+ if (!input.discussionTurn)
609
+ await emit("EXECUTION_DECIDED", {
610
+ phase: input.phase,
611
+ decision: "SKIP",
612
+ reason: "MODEL_ERROR",
613
+ stopReason: "error",
614
+ });
451
615
  await emit("RUN_FAILED", {
452
616
  phase: input.phase,
453
617
  code: error instanceof Error && /^[A-Z][A-Z0-9_]+$/u.test(error.message)
@@ -457,18 +621,12 @@ export class CodingRunManager {
457
621
  });
458
622
  })
459
623
  .finally(async () => {
460
- await emit.flush();
461
624
  try {
462
- await delegation?.stop();
463
- await delegation?.flush();
625
+ await emit.flush();
464
626
  }
465
627
  finally {
466
- await delegation?.dispose();
467
- await session.extensionRunner?.emit({ type: "session_shutdown", reason: "quit" });
468
- session.dispose();
628
+ await disposeRun();
469
629
  }
470
- if (this.active.get(runId) === activeRun)
471
- this.active.delete(runId);
472
630
  });
473
631
  }
474
632
  async synthetic(client, host, runId, turnId, input) {
@@ -504,27 +662,33 @@ export class CodingRunManager {
504
662
  treeHash: "a".repeat(40),
505
663
  branch: project.testBranch,
506
664
  })),
507
- testFacts: input.projects.map((project) => ({
508
- projectId: project.id,
509
- command: "pnpm test",
510
- exitCode: 0,
511
- commitSha: project.baselineCommit,
512
- treeHash: "a".repeat(40),
513
- finishedAt: new Date().toISOString(),
514
- })),
665
+ testFacts: [],
515
666
  });
516
667
  }
517
668
  }
518
- function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistSequence, redact = (value) => redactHostText(value)) {
519
- let sequence = initialSequence;
520
- 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;
521
685
  const emit = (kind, payload) => {
522
- queue = queue
686
+ state.queue = state.queue
523
687
  .catch(() => { })
524
688
  .then(async () => {
525
689
  const event = {
526
690
  eventId: crypto.randomUUID(),
527
- sequence: sequence + 1,
691
+ sequence: state.sequence + 1,
528
692
  turnId,
529
693
  kind,
530
694
  payload: kind === "PREPARATION_COMPLETED" ? payload : redactPayload(payload, redact),
@@ -537,8 +701,8 @@ function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistS
537
701
  generation: host.generation,
538
702
  events: [event],
539
703
  });
540
- sequence = event.sequence;
541
- await persistSequence?.(event.sequence);
704
+ state.sequence = event.sequence;
705
+ await state.persist?.(event.sequence);
542
706
  return;
543
707
  }
544
708
  catch (error) {
@@ -560,9 +724,9 @@ function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistS
560
724
  }
561
725
  }
562
726
  });
563
- return queue;
727
+ return state.queue;
564
728
  };
565
- return Object.assign(emit, { flush: () => queue });
729
+ return Object.assign(emit, { flush: () => state.queue });
566
730
  }
567
731
  function redactPayload(value, redact) {
568
732
  return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactPayloadValue(item, redact)]));
@@ -576,7 +740,7 @@ function redactPayloadValue(value, redact) {
576
740
  return redactPayload(value, redact);
577
741
  return value;
578
742
  }
579
- async function openPiSession(options) {
743
+ export async function openPiSession(options) {
580
744
  await mkdir(options.sessionDir, { recursive: true, mode: 0o700 });
581
745
  await mkdir(options.agentDir, { recursive: true, mode: 0o700 });
582
746
  let sessionManager;
@@ -623,11 +787,49 @@ async function openPiSession(options) {
623
787
  resourceLoader: loader,
624
788
  sessionManager,
625
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
+ }
626
806
  await session.bindExtensions({ mode: "print" });
627
807
  session.setAutoCompactionEnabled(true);
628
808
  return { session, sessionId };
629
809
  }
630
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();
631
833
  let messageId = crypto.randomUUID();
632
834
  let pendingText = "";
633
835
  let textFlushTimer;
@@ -655,10 +857,12 @@ function subscribeSession(session, emit, observed, redact) {
655
857
  };
656
858
  session.subscribe((event) => {
657
859
  if (event.type === "agent_start") {
860
+ publishContext();
658
861
  if (observed.messageQueue)
659
862
  void emit("MESSAGE_QUEUE_UPDATED", { mainAgentRunning: true });
660
863
  }
661
864
  else if (event.type === "agent_settled") {
865
+ publishContext();
662
866
  if (observed.messageQueue)
663
867
  void emit("MESSAGE_QUEUE_UPDATED", { mainAgentRunning: false });
664
868
  }
@@ -713,6 +917,7 @@ function subscribeSession(session, emit, observed, redact) {
713
917
  toolUpdateTimers.set(event.toolCallId, setTimeout(() => flushToolUpdate(event.toolCallId), 200));
714
918
  }
715
919
  else if (event.type === "tool_execution_end") {
920
+ publishContext();
716
921
  if (event.toolName === "subagent")
717
922
  return;
718
923
  flushToolUpdate(event.toolCallId);
@@ -737,6 +942,7 @@ function subscribeSession(session, emit, observed, redact) {
737
942
  toolUpdateTimers.set(activityKey, setTimeout(() => flushToolUpdate(activityKey), 200));
738
943
  }
739
944
  else if (event.type === "compaction_start") {
945
+ publishContext(true);
740
946
  flushText();
741
947
  void emit("TOOL_STARTED", {
742
948
  activityKey: `compaction:${event.reason}`,
@@ -745,6 +951,7 @@ function subscribeSession(session, emit, observed, redact) {
745
951
  });
746
952
  }
747
953
  else if (event.type === "compaction_end") {
954
+ publishContext();
748
955
  void emit("TOOL_COMPLETED", {
749
956
  activityKey: `compaction:${event.reason}`,
750
957
  toolName: "compaction",
@@ -755,6 +962,7 @@ function subscribeSession(session, emit, observed, redact) {
755
962
  });
756
963
  }
757
964
  else if (event.type === "message_end" && event.message.role === "assistant") {
965
+ publishContext();
758
966
  flushText();
759
967
  if ("stopReason" in event.message && event.message.stopReason)
760
968
  observed.lastModelStopReason = String(event.message.stopReason);
@@ -766,16 +974,8 @@ function subscribeSession(session, emit, observed, redact) {
766
974
  contentMarkdown: redact(content),
767
975
  messageId,
768
976
  };
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())
977
+ Object.assign(payload, localModelUsage(event.message, session.model));
978
+ if (content.trim() || usage)
779
979
  void emit("MESSAGE_COMPLETED", payload);
780
980
  }
781
981
  });
@@ -795,11 +995,6 @@ function textFromResult(value) {
795
995
  return textFromMessage(value.content);
796
996
  }
797
997
  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
998
  if (observed.stopping)
804
999
  return;
805
1000
  if (continuation && observed.userIntervened)
@@ -830,15 +1025,21 @@ async function driveToTerminal(session, initial, observed, client, host, runId,
830
1025
  await emit("EXECUTION_DECIDED", {
831
1026
  ...endFact,
832
1027
  decision: "SKIP",
833
- reason: "TERMINAL_ACCEPTED",
1028
+ reason: observed.deploymentCheckPaused ? "DEPLOYMENT_CHECK_PAUSED" : "TERMINAL_ACCEPTED",
834
1029
  model: null,
835
1030
  });
836
1031
  return;
837
1032
  }
838
1033
  if (observed.lastModelStopReason === "aborted" ||
839
1034
  (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");
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);
842
1043
  await emit("EXECUTION_DECIDED", {
843
1044
  ...endFact,
844
1045
  decision: "SKIP",
@@ -848,17 +1049,18 @@ async function driveToTerminal(session, initial, observed, client, host, runId,
848
1049
  if (observed.stopping)
849
1050
  return;
850
1051
  await client.reportCodingTerminal(runId, {
851
- ...host,
1052
+ hostId: host.hostId,
1053
+ generation: host.generation,
852
1054
  turnId,
853
1055
  kind: "FAILED",
854
- summary: observed.lastModelError ?? code,
1056
+ summary: message,
855
1057
  knownIssues: "",
856
1058
  failureCode: code,
857
1059
  projectFacts: [],
858
1060
  testFacts: [],
859
1061
  });
860
1062
  observed.terminalReached = true;
861
- await emit("RUN_FAILED", { phase, code, message: observed.lastModelError ?? code });
1063
+ await emit("RUN_FAILED", { phase, code, message });
862
1064
  return;
863
1065
  }
864
1066
  // Judge reads durable messages. It queues automatic work through the same command
@@ -906,169 +1108,40 @@ async function driveToTerminal(session, initial, observed, client, host, runId,
906
1108
  await completeConversation();
907
1109
  observed.terminalReached = true;
908
1110
  }
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
- },
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");
1068
1129
  });
1069
- };
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;
1070
1143
  }
1071
- export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects, resume) {
1144
+ export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects, resume, verifyRemote = false, uninitializedProjectIds = []) {
1072
1145
  await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
1073
1146
  for (const project of projects) {
1074
1147
  const mirror = join(cacheRoot, `${project.id}.git`);
@@ -1088,8 +1161,8 @@ export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects,
1088
1161
  (await canonicalRepository(project.repositoryUrl)))
1089
1162
  throw new Error("CODING_REPOSITORY_ORIGIN_MISMATCH");
1090
1163
  }
1091
- if (!resume) {
1092
- // 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.
1093
1166
  await execFileAsync("git", [
1094
1167
  "--git-dir",
1095
1168
  mirror,
@@ -1098,18 +1171,9 @@ export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects,
1098
1171
  "remote.origin.fetch",
1099
1172
  "+refs/heads/*:refs/remotes/origin/*",
1100
1173
  ]);
1101
- await execFileAsync("git", ["--git-dir", mirror, "fetch", "origin"]).catch(() => {
1174
+ await execFileAsync("git", ["--git-dir", mirror, "fetch", "--prune", "origin"]).catch(() => {
1102
1175
  throw new Error("CODING_REPOSITORY_ACCESS_FAILED");
1103
1176
  });
1104
- await execFileAsync("git", [
1105
- "--git-dir",
1106
- mirror,
1107
- "cat-file",
1108
- "-e",
1109
- `refs/remotes/origin/${project.baselineBranch}`,
1110
- ]).catch(() => {
1111
- throw new Error("CODING_BASELINE_BRANCH_UNAVAILABLE");
1112
- });
1113
1177
  }
1114
1178
  const target = join(runRoot, safeName(project.name));
1115
1179
  const expectedBranch = project.devBranch || `aip/${workItemKey}`;
@@ -1117,25 +1181,71 @@ export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects,
1117
1181
  if (!existingWorktree) {
1118
1182
  if (resume)
1119
1183
  throw new Error("CODING_RUN_WORKTREE_MISSING");
1120
- await execFileAsync("git", [
1184
+ const remoteExists = await execFileAsync("git", [
1121
1185
  "--git-dir",
1122
1186
  mirror,
1123
- "worktree",
1124
- "add",
1125
- "-b",
1126
- expectedBranch,
1127
- target,
1128
- `refs/remotes/origin/${project.baselineBranch}`,
1129
- ]).catch(() => {
1130
- throw new Error("CODING_WORKTREE_CREATE_FAILED");
1131
- });
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
+ }
1132
1240
  }
1133
- 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);
1134
1244
  }
1135
1245
  const onlyProject = projects.length === 1 ? projects[0] : null;
1136
1246
  return onlyProject ? join(runRoot, safeName(onlyProject.name)) : runRoot;
1137
1247
  }
1138
- async function validateWorktree(mirror, target, expectedBranch, resume) {
1248
+ async function validateWorktree(mirror, target, expectedBranch, resume, repositoryUrl) {
1139
1249
  const facts = await Promise.all([
1140
1250
  execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: target }),
1141
1251
  execFileAsync("git", ["rev-parse", "--git-common-dir"], { cwd: target }),
@@ -1151,8 +1261,15 @@ async function validateWorktree(mirror, target, expectedBranch, resume) {
1151
1261
  realpath(commonPath.startsWith("/") ? commonPath : resolve(target, commonPath)),
1152
1262
  realpath(mirror),
1153
1263
  ]);
1154
- if (actualCommon !== expectedCommon)
1155
- 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
+ }
1156
1273
  if (branch.trim() !== expectedBranch)
1157
1274
  throw new Error("CODING_WORKTREE_BRANCH_MISMATCH");
1158
1275
  if (resume)
@@ -1172,8 +1289,32 @@ async function canonicalRepository(value) {
1172
1289
  }
1173
1290
  return value.replace(/\/$/u, "");
1174
1291
  }
1175
- function executionInstructions() {
1176
- return "按当前任务和用户最新要求工作:自主选择工具,读取、修改、编译、测试和验证,完成阶段目标后调用提交工具。咨询、解释和有限操作按用户范围处理,不自动扩张为旧阶段任务。需要用户决定或外部信息时,用自然语言说明具体问题和已完成工作,不虚构答案、测试或交付成功。错误依据真实反馈修正;结果未知的有副作用操作先核实,不盲目重放。普通回复不等于交付,提交校验失败需要检查和修正,业务门禁仍须遵守。";
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)}`;
1177
1318
  }
1178
1319
  async function workspaceFingerprint(runRoot, input) {
1179
1320
  return JSON.stringify(await Promise.all(input.projects.map(async (project) => {
@@ -1186,44 +1327,12 @@ async function workspaceFingerprint(runRoot, input) {
1186
1327
  return [project.id, head.stdout.trim(), branch.stdout.trim(), tree];
1187
1328
  })));
1188
1329
  }
1189
- export function initialPrompt(input, runRoot, responsible = true) {
1190
- const directory = deliveryDirectory(runRoot, input);
1191
- if (input.executionPurpose === "RELEASE")
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。平台查询状态,不负责创建。仅说明或更新文档不退回研发;不执行实际发布、知识整理、关单。`;
1330
+ export function initialPrompt(input, runRoot, _responsible = true, materialRoot = runRoot) {
1331
+ const directory = deliveryDirectory(materialRoot, input);
1193
1332
  const projects = input.projects
1194
1333
  .map((p) => `- ${p.name}: ${join(runRoot, safeName(p.name))}(开发分支 ${p.devBranch} → 基准分支 ${p.baselineBranch};测试分支 ${p.testBranch})${p.id === input.primaryProjectId ? "(主项目)" : ""}`)
1195
1334
  .join("\n");
1196
- const phase = input.phase === "DEVELOPMENT" ? "DEVELOPMENT" : "INTEGRATION";
1197
- const phaseLabel = phase === "DEVELOPMENT" ? "开发与自审" : "系统测试,当前已进入系统测试,不是开发阶段";
1198
- const protocol = STAGE_DELIVERY_PROTOCOLS[phase];
1199
- const delivery = input.stageDeliverySnapshots?.[phase];
1200
- if (responsible && !delivery)
1201
- throw new Error("STAGE_DELIVERY_CONFIGURATION_MISSING");
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;本地副本未被覆盖,对照文件不收集。` : ""}`;
1203
- if (!responsible)
1204
- return context;
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}`;
1209
- }
1210
- async function uploadBatch(client, runId, batch) {
1211
- for (let attempt = 0;; attempt++) {
1212
- try {
1213
- await client.reportCodingTerminal(runId, batch);
1214
- return;
1215
- }
1216
- catch (error) {
1217
- if (error instanceof PlatformClientError && error.status < 500 && error.status !== 429)
1218
- throw error;
1219
- if (attempt >= 2)
1220
- throw new Error(`AIP_DELIVERY_UPLOAD_FAILED:同批材料尚未提交成功,可恢复重试;${error instanceof Error ? error.message : "网络错误"}`);
1221
- await new Promise((resolve) => setTimeout(resolve, 300 * (attempt + 1)));
1222
- }
1223
- }
1224
- }
1225
- function toolResult(text) {
1226
- 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)}`;
1227
1336
  }
1228
1337
  function safeName(value) {
1229
1338
  return basename(value).replace(/[^a-zA-Z0-9._-]/gu, "-") || "project";
@@ -1237,87 +1346,9 @@ async function exists(path) {
1237
1346
  return false;
1238
1347
  }
1239
1348
  }
1240
- async function collectProjectFacts(runRoot, projects, key, primaryProjectId) {
1241
- return Promise.all(projects.map(async (project) => {
1242
- const cwd = join(runRoot, safeName(project.name));
1243
- const excludeKey = project.id === primaryProjectId ? key : undefined;
1244
- const [{ stdout: head }, tree, { stdout: branch }] = await Promise.all([
1245
- execFileAsync("git", ["rev-parse", "HEAD"], { cwd }),
1246
- worktreeTreeHash(cwd, excludeKey),
1247
- execFileAsync("git", ["branch", "--show-current"], { cwd }),
1248
- ]);
1249
- const commitSha = head.trim();
1250
- return {
1251
- projectId: project.id,
1252
- projectName: project.name,
1253
- commitSha,
1254
- treeHash: tree,
1255
- branch: branch.trim(),
1256
- };
1257
- }));
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
- }
1282
1349
  async function captureProjectRevisions(runRoot, projects, key, primaryProjectId) {
1283
1350
  return collectProjectFacts(runRoot, projects, key, primaryProjectId);
1284
1351
  }
1285
- export async function worktreeTreeHash(cwd, key, committed = false, ref = "HEAD") {
1286
- const temporary = await mkdtemp(join(tmpdir(), "aip-tested-tree-"));
1287
- const index = join(temporary, "index");
1288
- const options = { cwd, env: { ...process.env, GIT_INDEX_FILE: index } };
1289
- try {
1290
- await execFileAsync("git", ["read-tree", ref], options);
1291
- if (!committed)
1292
- await execFileAsync("git", ["add", "-A"], options);
1293
- if (key) {
1294
- const names = [
1295
- "summary.md",
1296
- "prd.md",
1297
- "design.md",
1298
- "plan.md",
1299
- "test_plan.md",
1300
- "report.md",
1301
- "code_review.md",
1302
- "release_checklist.md",
1303
- "prd.platform.md",
1304
- "design.platform.md",
1305
- ];
1306
- await execFileAsync("git", [
1307
- "rm",
1308
- "--cached",
1309
- "--force", // This removes entries only from the dedicated temporary index.
1310
- "--ignore-unmatch",
1311
- "--",
1312
- ...names.map((name) => `docs/aip/${key}/${name}`),
1313
- ], options);
1314
- }
1315
- return (await execFileAsync("git", ["write-tree"], options)).stdout.trim();
1316
- }
1317
- finally {
1318
- await rm(temporary, { recursive: true, force: true });
1319
- }
1320
- }
1321
1352
  export function redactHostText(value, roots = []) {
1322
1353
  let redacted = value;
1323
1354
  for (const root of [...roots].filter(Boolean).sort((a, b) => b.length - a.length))
@@ -1333,4 +1364,84 @@ export function redactHostText(value, roots = []) {
1333
1364
  .replace(/\b[A-Za-z]:\\(?:[^\s'"`<>]+\\?)+/gu, "[本地路径]");
1334
1365
  return redacted.slice(0, 100_000);
1335
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
+ }
1336
1447
  //# sourceMappingURL=coding-run-manager.js.map