@bf-meishijian/aip 0.0.1 → 0.0.3

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 (44) hide show
  1. package/README.md +19 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +34 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/coding-run-manager.d.ts +76 -0
  6. package/dist/coding-run-manager.d.ts.map +1 -0
  7. package/dist/coding-run-manager.js +1090 -0
  8. package/dist/coding-run-manager.js.map +1 -0
  9. package/dist/daemon.d.ts +6 -0
  10. package/dist/daemon.d.ts.map +1 -1
  11. package/dist/daemon.js +58 -7
  12. package/dist/daemon.js.map +1 -1
  13. package/dist/delivery-files.d.ts +32 -0
  14. package/dist/delivery-files.d.ts.map +1 -0
  15. package/dist/delivery-files.js +100 -0
  16. package/dist/delivery-files.js.map +1 -0
  17. package/dist/diagnostics.js +1 -1
  18. package/dist/diagnostics.js.map +1 -1
  19. package/dist/installer.d.ts.map +1 -1
  20. package/dist/installer.js +5 -4
  21. package/dist/installer.js.map +1 -1
  22. package/dist/live-employee.d.ts +21 -0
  23. package/dist/live-employee.d.ts.map +1 -0
  24. package/dist/live-employee.js +184 -0
  25. package/dist/live-employee.js.map +1 -0
  26. package/dist/merge-requests.d.ts +48 -0
  27. package/dist/merge-requests.d.ts.map +1 -0
  28. package/dist/merge-requests.js +172 -0
  29. package/dist/merge-requests.js.map +1 -0
  30. package/dist/paths.d.ts +5 -0
  31. package/dist/paths.d.ts.map +1 -1
  32. package/dist/paths.js +5 -0
  33. package/dist/paths.js.map +1 -1
  34. package/dist/pi-model-service.d.ts +24 -9
  35. package/dist/pi-model-service.d.ts.map +1 -1
  36. package/dist/pi-model-service.js +104 -3
  37. package/dist/pi-model-service.js.map +1 -1
  38. package/dist/platform-client.d.ts +47 -0
  39. package/dist/platform-client.d.ts.map +1 -1
  40. package/dist/platform-client.js +51 -0
  41. package/dist/platform-client.js.map +1 -1
  42. package/dist/state.d.ts +2 -2
  43. package/dist/state.js +2 -2
  44. package/package.json +8 -6
@@ -0,0 +1,1090 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { mkdir, mkdtemp, open, realpath, rm, stat } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { basename, join, resolve } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { createAgentSession, DefaultResourceLoader, SessionManager, } from "@earendil-works/pi-coding-agent";
8
+ import { Type } from "typebox";
9
+ import { collectDeliveryFiles, deliveryDirectory, initializeDeliveryFiles, } from "./delivery-files.js";
10
+ import { liveEmployee, TEAM_AGENTS } from "./live-employee.js";
11
+ import { prepareMergeRequest, registerMergeRequest, } from "./merge-requests.js";
12
+ import { paths } from "./paths.js";
13
+ import { PlatformClientError } from "./platform-client.js";
14
+ import { readJson, writeSecureJson } from "./secure-json.js";
15
+ const execFileAsync = promisify(execFile);
16
+ export class CodingRunManager {
17
+ pi;
18
+ active = new Map();
19
+ constructor(pi) {
20
+ this.pi = pi;
21
+ }
22
+ isBusy() {
23
+ return this.active.size > 0;
24
+ }
25
+ async execute(client, host, command) {
26
+ if (!command.run)
27
+ throw new Error("CODING_RUN_COMMAND_INVALID");
28
+ const imageRun = command.run;
29
+ let images;
30
+ try {
31
+ images = await Promise.all((command.run.attachments ?? []).map((attachment) => client.attachmentContent(imageRun.runId, attachment.id, {
32
+ hostId: host.hostId,
33
+ generation: host.generation,
34
+ turnId: imageRun.turnId,
35
+ })));
36
+ }
37
+ catch (error) {
38
+ const active = this.active.get(imageRun.runId);
39
+ if (active?.turnId === imageRun.turnId) {
40
+ active.stopping = true;
41
+ active.observed.stopping = true;
42
+ await active.session.abort();
43
+ await active.finished;
44
+ if (this.active.get(imageRun.runId) === active)
45
+ this.active.delete(imageRun.runId);
46
+ }
47
+ throw error;
48
+ }
49
+ if (command.run.action === "START") {
50
+ const input = command.run.runInput;
51
+ await this.start(client, host, command.run.runId, command.run.turnId, input, false, command.run.contentMarkdown, images);
52
+ }
53
+ else if (command.run.action === "STEER") {
54
+ let active = this.active.get(command.run.runId);
55
+ if (!command.run.contentMarkdown && images.length === 0)
56
+ throw new Error("CODING_RUN_MESSAGE_MISSING");
57
+ if (active && active.turnId !== command.run.turnId) {
58
+ if (!active.observed.terminalReached)
59
+ throw new Error("CODING_RUN_TURN_MISMATCH");
60
+ active.stopping = true;
61
+ active.observed.stopping = true;
62
+ await active.session.abort();
63
+ if (this.active.get(command.run.runId) === active)
64
+ this.active.delete(command.run.runId);
65
+ active = undefined;
66
+ }
67
+ if (active) {
68
+ if (images.length > 0 && !active.session.model?.input.includes("image"))
69
+ throw new Error("MODEL_IMAGES_UNSUPPORTED");
70
+ await active.emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
71
+ if (images.length > 0)
72
+ await active.session.steer(command.run.contentMarkdown ?? "", images);
73
+ else
74
+ await active.session.steer(command.run.contentMarkdown ?? "");
75
+ await active.emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
76
+ }
77
+ else {
78
+ if (!command.run.runInput)
79
+ throw new Error("CODING_RUN_INPUT_MISSING");
80
+ await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
81
+ }
82
+ }
83
+ else if (command.run.action === "STOP") {
84
+ const active = this.active.get(command.run.runId);
85
+ if (active) {
86
+ if (active.turnId !== command.run.turnId)
87
+ throw new Error("CODING_RUN_TURN_MISMATCH");
88
+ active.stopping = true;
89
+ active.observed.stopping = true;
90
+ await active.session.abort();
91
+ for (const [activityKey, agentId] of active.observed.activeTasks) {
92
+ await active.emit("SUBAGENT_STOPPED", {
93
+ activityKey,
94
+ agentId,
95
+ summary: "随主执行停止",
96
+ });
97
+ }
98
+ active.observed.activeTasks.clear();
99
+ await active.emit("RUN_STOPPED", {});
100
+ }
101
+ else {
102
+ const stateFile = join(paths().runState, `${command.run.runId}.json`);
103
+ const state = await readJson(stateFile);
104
+ 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", {});
105
+ }
106
+ }
107
+ else if (command.run.action === "RESUME") {
108
+ await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
109
+ }
110
+ }
111
+ async start(client, host, runId, turnId, input, resume, resumePrompt, images = []) {
112
+ const active = this.active.get(runId);
113
+ if (active) {
114
+ if (active.turnId === turnId)
115
+ return;
116
+ if (!active.observed.terminalReached)
117
+ throw new Error("CODING_RUN_TURN_MISMATCH");
118
+ await active.finished;
119
+ if (this.active.get(runId) === active)
120
+ this.active.delete(runId);
121
+ }
122
+ if (process.env.AIP_TEST_PI === "1") {
123
+ await this.synthetic(client, host, runId, turnId, input);
124
+ return;
125
+ }
126
+ const local = paths();
127
+ const runRoot = join(local.runWorktrees, runId);
128
+ await mkdir(runRoot, { recursive: true, mode: 0o700 });
129
+ await mkdir(local.piSessions, { recursive: true, mode: 0o700 });
130
+ await mkdir(local.piAgent, { recursive: true, mode: 0o700 });
131
+ const stateFile = join(local.runState, `${runId}.json`);
132
+ const storedState = await readJson(stateFile);
133
+ const cwd = await prepareProjects(local.repositoryCache, runRoot, runId, input.projects, Boolean(storedState?.prepared));
134
+ for (const project of input.projects)
135
+ project.devBranch ||= `aip/${runId}/${project.id.slice(0, 8)}`;
136
+ if (!storedState?.prepared || input.inputConflict)
137
+ await initializeDeliveryFiles(runRoot, input, Boolean(storedState?.prepared));
138
+ const deliveryInputHash = createHash("sha256")
139
+ .update(JSON.stringify([
140
+ input.primaryProjectId,
141
+ input.requirement.body_markdown,
142
+ input.technicalDesign?.content,
143
+ ]))
144
+ .digest("hex");
145
+ const childSessions = storedState?.childSessions ?? {};
146
+ let pendingDelivery = storedState?.phase !== input.phase ||
147
+ resumePrompt ||
148
+ images.length > 0 ||
149
+ (storedState?.deliveryInputHash && storedState.deliveryInputHash !== deliveryInputHash)
150
+ ? undefined
151
+ : storedState?.pendingDelivery
152
+ ? {
153
+ ...storedState.pendingDelivery,
154
+ turnId,
155
+ hostId: host.hostId,
156
+ generation: host.generation,
157
+ }
158
+ : undefined;
159
+ const phaseSessions = storedState?.phaseSessions ?? {};
160
+ const sessionPhase = input.executionPurpose === "RELEASE" ? "DEVELOPMENT" : input.phase;
161
+ let sessionFileRef = phaseSessions[sessionPhase];
162
+ let lastEventSequence = storedState?.lastEventSequence ?? 0;
163
+ const observed = {
164
+ pendingQuestion: storedState?.phase === input.phase ? (storedState.pendingQuestion ?? "") : "",
165
+ memberResults: new Map(),
166
+ activeTasks: new Map(),
167
+ stopping: false,
168
+ terminalReached: false,
169
+ };
170
+ const persistState = () => writeSecureJson(stateFile, {
171
+ phase: input.phase,
172
+ mrContext: {
173
+ runId,
174
+ turnId,
175
+ workItem: input.workItem,
176
+ projects: input.projects,
177
+ primaryProjectId: input.primaryProjectId,
178
+ },
179
+ prepared: true,
180
+ phaseSessions,
181
+ sessionFile: sessionFileRef,
182
+ lastEventSequence,
183
+ pendingQuestion: observed.pendingQuestion,
184
+ childSessions,
185
+ pendingDelivery,
186
+ deliveryInputHash,
187
+ });
188
+ const redact = (value) => redactHostText(value, [runRoot, local.root]);
189
+ const emit = eventEmitter(client, host, runId, turnId, lastEventSequence, (sequence) => {
190
+ lastEventSequence = sequence;
191
+ return persistState();
192
+ }, redact);
193
+ if (input.phase === "PREPARATION") {
194
+ await persistState();
195
+ await emit("PREPARATION_COMPLETED", {
196
+ primaryProjectId: input.primaryProjectId,
197
+ projects: await Promise.all(input.projects.map(async (p) => ({
198
+ projectId: p.id,
199
+ projectName: p.name,
200
+ baselineCommit: (await execFileAsync("git", ["rev-parse", "HEAD"], {
201
+ cwd: join(runRoot, safeName(p.name)),
202
+ })).stdout.trim(),
203
+ directory: join(runRoot, safeName(p.name)),
204
+ branch: (await execFileAsync("git", ["branch", "--show-current"], {
205
+ cwd: join(runRoot, safeName(p.name)),
206
+ })).stdout.trim(),
207
+ }))),
208
+ });
209
+ return;
210
+ }
211
+ const terminal = terminalExtension(client, host, runId, turnId, runRoot, input, observed, redact, async (batch) => {
212
+ pendingDelivery = batch;
213
+ await persistState();
214
+ }, () => pendingDelivery, (question) => emit("QUESTION_UPDATED", { question: redact(question) }), () => emit.flush());
215
+ if (!this.pi.resolveRuntimeModel)
216
+ throw new Error("PI_RUNTIME_UNAVAILABLE");
217
+ const resolved = await this.pi.resolveRuntimeModel(input.providerId, input.modelId, {
218
+ runId,
219
+ turnId,
220
+ hostId: host.hostId,
221
+ generation: host.generation,
222
+ });
223
+ if (images.length > 0 && !resolved.model.input?.includes("image"))
224
+ throw new Error("MODEL_IMAGES_UNSUPPORTED");
225
+ const binding = {
226
+ hostId: host.hostId,
227
+ generation: host.generation,
228
+ turnId,
229
+ member: "supervisor",
230
+ };
231
+ const availableAgents = Object.entries(TEAM_AGENTS)
232
+ .filter(([member]) => input.team?.members[`${member}Id`])
233
+ .map(([, name]) => name);
234
+ const live = liveEmployee(client, runId, binding, await client.employeeConfig(runId, { ...binding, purpose: "LOAD" }), runRoot, availableAgents.length > 0, input.executionPurpose === "RELEASE", input.phase, () => observed.terminalReached);
235
+ const childInstances = new Map();
236
+ const childUpdates = new Map();
237
+ const childTasks = new Map();
238
+ const team = (api) => {
239
+ api.registerTool({
240
+ name: "subagent",
241
+ label: "派发研发团队",
242
+ description: `向实际绑定员工分派子任务,返回真实子会话结果。可用员工:${availableAgents.join("、")}。`,
243
+ parameters: Type.Object({
244
+ agent: Type.Union(availableAgents.map((name) => Type.Literal(name))),
245
+ task: Type.String({ minLength: 1 }),
246
+ }),
247
+ async execute(callId, params, signal, onUpdate) {
248
+ const member = Object.entries(TEAM_AGENTS).find(([, name]) => name === params.agent)?.[0];
249
+ childTasks.set(member, callId);
250
+ const childBinding = { ...binding, member };
251
+ let child = childInstances.get(member);
252
+ observed.memberResults.delete(params.agent);
253
+ childUpdates.set(member, (text) => onUpdate?.({ content: [{ type: "text", text }], details: { agent: params.agent } }));
254
+ if (!child) {
255
+ const childLive = liveEmployee(client, runId, childBinding, await client.employeeConfig(runId, { ...childBinding, purpose: "LOAD" }), runRoot, true, false, input.phase);
256
+ const resultExtension = (pi) => pi.registerTool({
257
+ name: "submit_member_result",
258
+ label: "提交成员结果",
259
+ description: "如实记录本次子任务的结果,未解决的问题必须 FAIL。仅向主管汇报,不确认最终交付。",
260
+ parameters: Type.Object({
261
+ status: Type.Union([Type.Literal("PASS"), Type.Literal("FAIL")]),
262
+ summary: Type.String({ minLength: 1 }),
263
+ }),
264
+ async execute(_id, result) {
265
+ const revisions = (await captureProjectRevisions(runRoot, input.projects, input.workItem.key, input.primaryProjectId)).filter((r) => Boolean(r));
266
+ observed.memberResults.set(params.agent, {
267
+ status: result.status,
268
+ summary: result.summary,
269
+ revisions,
270
+ });
271
+ return toolResult("结果已记录,向主管返回结论。");
272
+ },
273
+ });
274
+ const childLoader = new DefaultResourceLoader({
275
+ cwd,
276
+ agentDir: local.piAgent,
277
+ noExtensions: true,
278
+ noSkills: true,
279
+ noPromptTemplates: true,
280
+ noThemes: true,
281
+ noContextFiles: true,
282
+ extensionFactories: [childLive.extension, resultExtension],
283
+ systemPrompt: "你是研发团队中的独立数字员工,按本次固定职责工作。",
284
+ });
285
+ await childLoader.reload();
286
+ const file = childSessions[`${input.phase}:${member}`];
287
+ const childManager = file
288
+ ? SessionManager.open(join(local.piSessions, file), local.piSessions, cwd)
289
+ : SessionManager.create(cwd, local.piSessions);
290
+ const created = await createAgentSession({
291
+ cwd,
292
+ agentDir: local.piAgent,
293
+ modelRuntime: resolved.runtime,
294
+ model: resolved.model,
295
+ thinkingLevel: input.thinkingLevel,
296
+ resourceLoader: childLoader,
297
+ sessionManager: childManager,
298
+ });
299
+ child = created.session;
300
+ childLive.attach(child);
301
+ childInstances.set(member, child);
302
+ const childFile = childManager.getSessionFile();
303
+ if (!childFile)
304
+ throw new Error("CHILD_SESSION_FILE_MISSING");
305
+ childSessions[`${input.phase}:${member}`] = basename(childFile);
306
+ await persistState();
307
+ subscribeSession(child, async (kind, payload) => {
308
+ if (kind === "MESSAGE_STARTED")
309
+ return;
310
+ if (kind === "TEXT_DELTA") {
311
+ childUpdates.get(member)?.(String(payload.text ?? ""));
312
+ return;
313
+ }
314
+ await emit(kind, {
315
+ ...payload,
316
+ agentId: params.agent,
317
+ phase: input.phase,
318
+ parentActivityKey: childTasks.get(member),
319
+ sessionId: childSessions[`${input.phase}:${member}`],
320
+ });
321
+ }, observed, redact);
322
+ }
323
+ await emit("SUBAGENT_UPDATED", {
324
+ phase: input.phase,
325
+ task: params.task,
326
+ activityKey: callId,
327
+ agentId: params.agent,
328
+ sessionFileRef: childSessions[`${input.phase}:${member}`],
329
+ summary: "独立子会话已就绪",
330
+ });
331
+ const abort = () => {
332
+ void child?.abort();
333
+ };
334
+ signal?.addEventListener("abort", abort, { once: true });
335
+ try {
336
+ await child.prompt(`${initialPrompt(input, runRoot)}\n\n本次主管分派给 ${params.agent} 的任务:${params.task}\n只完成自身职责,最后调用 submit_member_result;主管负责其他成员协调和最终交付。`);
337
+ await child.waitForIdle();
338
+ if (signal?.aborted || observed.stopping)
339
+ throw new Error("SUBAGENT_STOPPED");
340
+ const result = observed.memberResults.get(params.agent);
341
+ if (!result)
342
+ throw new Error("SUBAGENT_RESULT_MISSING");
343
+ return {
344
+ content: [{ type: "text", text: JSON.stringify(result) }],
345
+ details: {
346
+ agent: params.agent,
347
+ status: result.status,
348
+ sessionFileRef: childSessions[`${input.phase}:${member}`],
349
+ },
350
+ };
351
+ }
352
+ finally {
353
+ childUpdates.delete(member);
354
+ signal?.removeEventListener("abort", abort);
355
+ }
356
+ },
357
+ });
358
+ };
359
+ const loader = new DefaultResourceLoader({
360
+ cwd,
361
+ agentDir: local.piAgent,
362
+ extensionFactories: [...(availableAgents.length ? [team] : []), terminal, live.extension],
363
+ noSkills: true,
364
+ noExtensions: true,
365
+ noPromptTemplates: true,
366
+ noThemes: true,
367
+ noContextFiles: true,
368
+ systemPrompt: input.team
369
+ ? "你是研发团队的主管,由平台提供固定团队身份和实时能力配置。"
370
+ : "你是负责本工单实现、自审、测试与最终交付的数字员工。",
371
+ });
372
+ await loader.reload();
373
+ const existingSession = phaseSessions[sessionPhase]
374
+ ? join(local.piSessions, phaseSessions[sessionPhase])
375
+ : null;
376
+ let sessionManager;
377
+ if (existingSession) {
378
+ sessionManager = SessionManager.open(existingSession, local.piSessions, cwd);
379
+ }
380
+ else {
381
+ sessionManager = SessionManager.create(cwd, local.piSessions);
382
+ const allocatedSessionFile = sessionManager.getSessionFile();
383
+ if (!allocatedSessionFile)
384
+ throw new Error("PI_SESSION_FILE_MISSING");
385
+ const placeholder = await open(allocatedSessionFile, "wx", 0o600);
386
+ await placeholder.close();
387
+ sessionManager.setSessionFile(allocatedSessionFile);
388
+ sessionManager.appendCustomEntry("aip-run-binding", {
389
+ runId,
390
+ });
391
+ }
392
+ const sessionFile = sessionManager.getSessionFile();
393
+ if (!sessionFile)
394
+ throw new Error("PI_SESSION_FILE_MISSING");
395
+ if (!(await exists(sessionFile)))
396
+ throw new Error("PI_SESSION_FILE_NOT_PERSISTED");
397
+ const { session } = await createAgentSession({
398
+ cwd,
399
+ agentDir: local.piAgent,
400
+ modelRuntime: resolved.runtime,
401
+ model: resolved.model,
402
+ thinkingLevel: input.thinkingLevel,
403
+ resourceLoader: loader,
404
+ sessionManager,
405
+ });
406
+ live.attach(session);
407
+ const activeRun = { session, input, turnId, emit, stopping: false, observed };
408
+ this.active.set(runId, activeRun);
409
+ subscribeSession(session, (kind, payload) => emit(kind, {
410
+ ...payload,
411
+ phase: input.phase,
412
+ agentId: payload.agentId ?? "supervisor",
413
+ sessionId: sessionFileRef,
414
+ }), observed, redact);
415
+ sessionFileRef = basename(sessionFile);
416
+ phaseSessions[sessionPhase] = sessionFileRef;
417
+ await persistState();
418
+ await emit("RUN_STARTED", { sessionFileRef, phase: input.phase });
419
+ if (resumePrompt || images.length > 0) {
420
+ await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
421
+ await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
422
+ }
423
+ const prompt = `${initialPrompt(input, runRoot)}${resumePrompt ? `\n\n${resumePrompt}` : resume ? "\n\n继续当前会话未完成的工作,先读取最新文件和交接结果。" : ""}`;
424
+ activeRun.finished = driveToTerminal(session, prompt, observed, client, host, runId, turnId, emit, input.executionPurpose === "RELEASE", input.phase, images)
425
+ .catch(async (error) => {
426
+ if (activeRun.stopping)
427
+ return;
428
+ await emit("RUN_FAILED", {
429
+ code: error instanceof Error ? error.message.slice(0, 200) : "PI_RUN_FAILED",
430
+ });
431
+ })
432
+ .finally(async () => {
433
+ await emit.flush();
434
+ if (this.active.get(runId) === activeRun)
435
+ this.active.delete(runId);
436
+ });
437
+ }
438
+ async synthetic(client, host, runId, turnId, input) {
439
+ const emit = eventEmitter(client, host, runId, turnId);
440
+ await emit("RUN_STARTED", { sessionFileRef: `${runId}.jsonl` });
441
+ await emit("SUBAGENT_STARTED", {
442
+ activityKey: "reviewer-1",
443
+ agentId: "platform-reviewer",
444
+ summary: "审查实现",
445
+ });
446
+ await emit("SUBAGENT_COMPLETED", {
447
+ activityKey: "reviewer-1",
448
+ agentId: "platform-reviewer",
449
+ summary: "审查通过",
450
+ });
451
+ await emit("MESSAGE_COMPLETED", {
452
+ contentMarkdown: "实现、测试与代码审查已经完成,等待确认交付。",
453
+ });
454
+ await client.reportCodingTerminal(runId, {
455
+ hostId: host.hostId,
456
+ generation: host.generation,
457
+ turnId,
458
+ kind: "DELIVERY",
459
+ summary: "本地 Coding Run 验收交付",
460
+ knownIssues: "",
461
+ projectFacts: input.projects.map((project) => ({
462
+ projectId: project.id,
463
+ projectName: project.name,
464
+ commitSha: project.baselineCommit,
465
+ treeHash: "a".repeat(40),
466
+ branch: project.testBranch,
467
+ })),
468
+ testFacts: input.projects.map((project) => ({
469
+ projectId: project.id,
470
+ command: "pnpm test",
471
+ exitCode: 0,
472
+ commitSha: project.baselineCommit,
473
+ treeHash: "a".repeat(40),
474
+ finishedAt: new Date().toISOString(),
475
+ })),
476
+ });
477
+ }
478
+ }
479
+ function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistSequence, redact = (value) => redactHostText(value)) {
480
+ let sequence = initialSequence;
481
+ let queue = Promise.resolve();
482
+ const emit = (kind, payload) => {
483
+ sequence += 1;
484
+ const event = {
485
+ eventId: crypto.randomUUID(),
486
+ sequence,
487
+ turnId,
488
+ kind,
489
+ payload: kind === "PREPARATION_COMPLETED" ? payload : redactPayload(payload, redact),
490
+ createdAt: new Date().toISOString(),
491
+ };
492
+ queue = queue
493
+ .then(() => client.appendCodingEvents(runId, {
494
+ hostId: host.hostId,
495
+ generation: host.generation,
496
+ events: [event],
497
+ }))
498
+ .then(() => persistSequence?.(event.sequence))
499
+ .then(() => undefined)
500
+ .catch((error) => {
501
+ if (error instanceof PlatformClientError &&
502
+ error.status === 409 &&
503
+ error.code === "CODING_EVENT_TURN_STALE")
504
+ return;
505
+ throw error;
506
+ });
507
+ return queue;
508
+ };
509
+ return Object.assign(emit, { flush: () => queue });
510
+ }
511
+ function redactPayload(value, redact) {
512
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactPayloadValue(item, redact)]));
513
+ }
514
+ function redactPayloadValue(value, redact) {
515
+ if (typeof value === "string")
516
+ return redact(value);
517
+ if (Array.isArray(value))
518
+ return value.map((item) => redactPayloadValue(item, redact));
519
+ if (value && typeof value === "object")
520
+ return redactPayload(value, redact);
521
+ return value;
522
+ }
523
+ async function driveToTerminal(session, initial, observed, client, host, runId, turnId, emit, release, phase, images = []) {
524
+ let prompt = initial;
525
+ for (let attempt = 0; attempt <= 2; attempt += 1) {
526
+ if (!session.model?.input.includes("image") &&
527
+ session.messages?.some((message) => "content" in message &&
528
+ Array.isArray(message.content) &&
529
+ message.content.some((block) => block.type === "image")))
530
+ throw new Error("MODEL_IMAGES_UNSUPPORTED");
531
+ if (attempt === 0 && images.length > 0)
532
+ await session.prompt(prompt, { images });
533
+ else
534
+ await session.prompt(prompt);
535
+ await session.waitForIdle();
536
+ if (observed.stopping || observed.terminalReached || observed.pendingQuestion)
537
+ return;
538
+ if (attempt < 2) {
539
+ prompt = release
540
+ ? `请调用 submit_pull_requests 完成提 PR;最近错误:${observed.lastDeliveryError ?? "尚未提交"}。`
541
+ : `你尚未成功交付。自主补齐必需产物后调用 ${phase === "DEVELOPMENT" ? "complete_development" : "submit_delivery"}。最近提交结果:${observed.lastDeliveryError ?? "尚未提交"}。缺文件就在当前会话补齐,传输故障不要重写报告。无法继续时说明真实原因,需要沟通可记录待回应问题。`;
542
+ continue;
543
+ }
544
+ await client.reportCodingTerminal(runId, {
545
+ hostId: host.hostId,
546
+ generation: host.generation,
547
+ turnId,
548
+ kind: "FAILED",
549
+ summary: observed.lastDeliveryError ?? "Agent 多次结束但没有形成明确终态",
550
+ knownIssues: "",
551
+ failureCode: observed.lastDeliveryError?.split("\n")[0]?.slice(0, 200) ?? "PI_TERMINAL_TOOL_NOT_CALLED",
552
+ projectFacts: [],
553
+ testFacts: [],
554
+ });
555
+ observed.terminalReached = true;
556
+ await emit("RUN_FAILED", { code: "PI_TERMINAL_TOOL_NOT_CALLED" });
557
+ }
558
+ }
559
+ function terminalExtension(client, host, runId, turnId, runRoot, input, observed, redact, persistBatch, pendingBatch, emitQuestion, flushEvents) {
560
+ return (pi) => {
561
+ pi.registerTool({
562
+ name: input.executionPurpose === "RELEASE"
563
+ ? "submit_pull_requests"
564
+ : input.phase === "DEVELOPMENT"
565
+ ? "complete_development"
566
+ : "submit_delivery",
567
+ label: "提交交付",
568
+ description: input.executionPurpose === "RELEASE"
569
+ ? "核实上线清单并提交分支后,使用 Runtime GitLab 身份创建或复用 MR 并登记工单。"
570
+ : "开发、review、测试完成后提交交付事实。",
571
+ parameters: input.executionPurpose === "RELEASE"
572
+ ? Type.Object({
573
+ checklistSummary: Type.String({
574
+ description: "核实依据、无需修改或修改摘要;缺口未解决时不要提交",
575
+ }),
576
+ projects: Type.Array(Type.Object({
577
+ project: Type.String({
578
+ description: `项目名称,只能选择:${input.projects.map((p) => p.name).join("、")}`,
579
+ }),
580
+ title: Type.String({ description: "包含工单编号及本项目变更摘要的 MR 标题" }),
581
+ description: Type.String({
582
+ description: "任务目的、本项目改动、测试与验收结论、发布注意、依赖、风险回滚及清单和报告链接",
583
+ }),
584
+ })),
585
+ })
586
+ : Type.Object({}),
587
+ async execute(_toolCallId, rawParams) {
588
+ try {
589
+ if (input.executionPurpose === "RELEASE") {
590
+ const params = rawParams;
591
+ if (!params?.checklistSummary?.trim())
592
+ throw new Error("AIP_RELEASE_CHECKLIST_SUMMARY_REQUIRED");
593
+ const files = await collectDeliveryFiles(runRoot, input, ["RELEASE_CHECKLIST"]);
594
+ const checklist = {
595
+ status: "VERIFIED",
596
+ summary: params.checklistSummary,
597
+ markdown: files[0].content,
598
+ };
599
+ await client.submitPullRequests(runId, {
600
+ hostId: host.hostId,
601
+ generation: host.generation,
602
+ turnId,
603
+ workItemId: input.workItem.id,
604
+ projects: [],
605
+ checklist,
606
+ });
607
+ const context = {
608
+ runId,
609
+ turnId,
610
+ workItem: input.workItem,
611
+ projects: input.projects,
612
+ primaryProjectId: input.primaryProjectId,
613
+ };
614
+ for (const project of input.projects) {
615
+ const explanation = params.projects?.find((item) => item.project === project.name);
616
+ if (!explanation)
617
+ throw new Error(`AIP_MR_DESCRIPTION_REQUIRED:${project.name}`);
618
+ const pendingFile = join(runRoot, `mr-pending-${safeName(project.id)}.json`);
619
+ const result = await prepareMergeRequest({
620
+ project,
621
+ cwd: join(runRoot, safeName(project.name)),
622
+ title: explanation.title,
623
+ description: explanation.description,
624
+ pendingFile,
625
+ });
626
+ await registerMergeRequest(client, context, host, result, pendingFile);
627
+ }
628
+ await client.submitPullRequests(runId, {
629
+ hostId: host.hostId,
630
+ generation: host.generation,
631
+ turnId,
632
+ workItemId: input.workItem.id,
633
+ projects: [],
634
+ completeRelease: true,
635
+ });
636
+ observed.terminalReached = true;
637
+ return toolResult("上线清单已核实,MR 已提交并登记,等待合并;平台负责查询最新状态。");
638
+ }
639
+ const existing = pendingBatch();
640
+ if (existing && existing.turnId === turnId) {
641
+ await flushEvents();
642
+ await uploadBatch(client, runId, existing);
643
+ await persistBatch(undefined);
644
+ observed.terminalReached = true;
645
+ return toolResult("交付已提交,等待经办人确认。");
646
+ }
647
+ const files = await collectDeliveryFiles(runRoot, input, input.phase === "DEVELOPMENT"
648
+ ? ["SUMMARY", "CODE_REVIEW", "IMPLEMENTATION_PLAN", "RELEASE_CHECKLIST"]
649
+ : undefined);
650
+ const projectFacts = await collectProjectFacts(runRoot, input.projects, input.workItem.key, input.primaryProjectId);
651
+ const batch = {
652
+ hostId: host.hostId,
653
+ generation: host.generation,
654
+ turnId,
655
+ kind: input.phase === "DEVELOPMENT" ? "DEVELOPMENT_COMPLETED" : "DELIVERY",
656
+ submissionId: crypto.randomUUID(),
657
+ files,
658
+ summary: files.find((file) => file.slot === "SUMMARY").content,
659
+ knownIssues: "",
660
+ projectFacts,
661
+ testFacts: [],
662
+ };
663
+ await persistBatch(batch);
664
+ await flushEvents();
665
+ await uploadBatch(client, runId, batch);
666
+ await persistBatch(undefined);
667
+ observed.terminalReached = true;
668
+ return toolResult("交付已提交,等待经办人确认。");
669
+ }
670
+ catch (error) {
671
+ observed.lastDeliveryError =
672
+ error instanceof Error ? error.message : "AIP_DELIVERY_FAILED";
673
+ if (error instanceof PlatformClientError && error.status < 500 && error.status !== 429)
674
+ await persistBatch(undefined);
675
+ throw error;
676
+ }
677
+ },
678
+ });
679
+ pi.registerTool({
680
+ name: "update_question",
681
+ label: "更新待回应问题",
682
+ description: "仅记录当前执行必须由人决定的业务问题,不把一般建议变为审批;真正解决后传空字符串清除。普通聊天继续使用 steer,不停止 Run。",
683
+ parameters: Type.Object({ question: Type.String() }),
684
+ async execute(_id, params) {
685
+ // The caller binding supplies technical identity; the model only owns the question text.
686
+ observed.pendingQuestion = params.question.trim();
687
+ await emitQuestion(params.question);
688
+ return toolResult(params.question);
689
+ },
690
+ });
691
+ if (input.executionPurpose === "RELEASE")
692
+ pi.registerTool({
693
+ name: "request_code_changes",
694
+ label: "返回开发修改",
695
+ description: "用户明确要求修改应用代码时,将工单返回开发,再测试和人工确认;仅讨论或文档修改不要调用。",
696
+ parameters: Type.Object({
697
+ reason: Type.String({ description: "用户要求修改的内容及原因" }),
698
+ }),
699
+ async execute(_id, params) {
700
+ await client.request(`/api/v1/coding-runs/${runId}/return-to-development`, {
701
+ method: "POST",
702
+ body: JSON.stringify({
703
+ hostId: host.hostId,
704
+ generation: host.generation,
705
+ turnId,
706
+ reason: params.reason,
707
+ }),
708
+ });
709
+ observed.terminalReached = true;
710
+ return toolResult("已返回开发,等待下一执行接手续改,完成后进入测试与人工确认。");
711
+ },
712
+ });
713
+ pi.registerTool({
714
+ name: "report_blocker",
715
+ label: "报告阻塞",
716
+ description: "仅在认证、权限、仓库、外部环境或输入冲突真实阻塞时报告。",
717
+ parameters: Type.Object({
718
+ category: Type.Union([
719
+ Type.Literal("AUTHENTICATION"),
720
+ Type.Literal("PERMISSION"),
721
+ Type.Literal("REPOSITORY"),
722
+ Type.Literal("EXTERNAL_ENVIRONMENT"),
723
+ Type.Literal("INPUT_CONFLICT"),
724
+ ]),
725
+ summary: Type.String(),
726
+ }),
727
+ async execute(_id, params) {
728
+ await client.reportCodingTerminal(runId, {
729
+ hostId: host.hostId,
730
+ generation: host.generation,
731
+ turnId,
732
+ kind: "BLOCKED",
733
+ summary: redact(params.summary),
734
+ knownIssues: "",
735
+ blockerCategory: params.category,
736
+ projectFacts: [],
737
+ testFacts: [],
738
+ });
739
+ observed.terminalReached = true;
740
+ return toolResult("阻塞已报告。");
741
+ },
742
+ });
743
+ };
744
+ }
745
+ function subscribeSession(session, emit, observed, redact) {
746
+ let messageId = crypto.randomUUID();
747
+ let pendingText = "";
748
+ let textFlushTimer;
749
+ const pendingToolUpdates = new Map();
750
+ const toolUpdateTimers = new Map();
751
+ const flushText = () => {
752
+ if (textFlushTimer)
753
+ clearTimeout(textFlushTimer);
754
+ textFlushTimer = undefined;
755
+ if (!pendingText)
756
+ return;
757
+ const text = pendingText;
758
+ pendingText = "";
759
+ void emit("TEXT_DELTA", { text, messageId });
760
+ };
761
+ const flushToolUpdate = (activityKey) => {
762
+ const timer = toolUpdateTimers.get(activityKey);
763
+ if (timer)
764
+ clearTimeout(timer);
765
+ toolUpdateTimers.delete(activityKey);
766
+ const update = pendingToolUpdates.get(activityKey);
767
+ pendingToolUpdates.delete(activityKey);
768
+ if (update)
769
+ void emit(update.kind, update.payload);
770
+ };
771
+ session.subscribe((event) => {
772
+ if (event.type === "message_start" && event.message.role === "assistant") {
773
+ messageId = crypto.randomUUID();
774
+ void emit("MESSAGE_STARTED", { messageId });
775
+ }
776
+ else if (event.type === "message_update" &&
777
+ event.assistantMessageEvent.type === "text_delta" &&
778
+ event.assistantMessageEvent.delta) {
779
+ pendingText += redact(event.assistantMessageEvent.delta);
780
+ if (pendingText.length >= 4_000)
781
+ flushText();
782
+ else
783
+ textFlushTimer ??= setTimeout(flushText, 200);
784
+ }
785
+ else if (event.type === "tool_execution_start") {
786
+ flushText();
787
+ const subagent = event.toolName === "subagent";
788
+ if (subagent)
789
+ observed.activeTasks.set(event.toolCallId, String(event.args?.agent));
790
+ void emit(subagent ? "SUBAGENT_STARTED" : "TOOL_STARTED", subagent
791
+ ? {
792
+ activityKey: event.toolCallId,
793
+ agentId: String(event.args?.agent ?? "platform-reviewer"),
794
+ summary: String(event.args?.task ?? "团队子任务开始"),
795
+ task: String(event.args?.task ?? ""),
796
+ }
797
+ : {
798
+ activityKey: event.toolCallId,
799
+ toolName: event.toolName,
800
+ summary: `执行 ${event.toolName}`,
801
+ });
802
+ }
803
+ else if (event.type === "tool_execution_update") {
804
+ pendingToolUpdates.set(event.toolCallId, event.toolName === "subagent"
805
+ ? {
806
+ kind: "SUBAGENT_UPDATED",
807
+ payload: {
808
+ activityKey: event.toolCallId,
809
+ agentId: String(event.args?.agent ?? "platform-reviewer"),
810
+ currentAction: redact(textFromResult(event.partialResult)),
811
+ },
812
+ }
813
+ : {
814
+ kind: "TOOL_UPDATED",
815
+ payload: {
816
+ activityKey: event.toolCallId,
817
+ toolName: event.toolName,
818
+ currentAction: redact(textFromResult(event.partialResult)),
819
+ },
820
+ });
821
+ if (!toolUpdateTimers.has(event.toolCallId))
822
+ toolUpdateTimers.set(event.toolCallId, setTimeout(() => flushToolUpdate(event.toolCallId), 200));
823
+ }
824
+ else if (event.type === "tool_execution_end") {
825
+ flushToolUpdate(event.toolCallId);
826
+ if (event.toolName === "subagent")
827
+ observed.activeTasks.delete(event.toolCallId);
828
+ void emit(event.toolName === "subagent"
829
+ ? observed.stopping
830
+ ? "SUBAGENT_STOPPED"
831
+ : event.isError
832
+ ? "SUBAGENT_FAILED"
833
+ : "SUBAGENT_COMPLETED"
834
+ : "TOOL_COMPLETED", event.toolName === "subagent"
835
+ ? {
836
+ activityKey: event.toolCallId,
837
+ agentId: String(event.result?.details?.agent ??
838
+ "platform-reviewer"),
839
+ summary: redact(textFromResult(event.result)),
840
+ }
841
+ : {
842
+ activityKey: event.toolCallId,
843
+ toolName: event.toolName,
844
+ isError: event.isError === true,
845
+ summary: redact(textFromResult(event.result)),
846
+ });
847
+ }
848
+ else if (event.type === "message_end" && event.message.role === "assistant") {
849
+ flushText();
850
+ const content = textFromMessage(event.message.content);
851
+ if (content)
852
+ void emit("MESSAGE_COMPLETED", { contentMarkdown: redact(content), messageId });
853
+ }
854
+ });
855
+ }
856
+ export async function prepareProjects(cacheRoot, runRoot, runId, projects, resume) {
857
+ await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
858
+ for (const project of projects) {
859
+ const mirror = join(cacheRoot, `${project.id}.git`);
860
+ if (!(await exists(mirror))) {
861
+ await execFileAsync("git", ["clone", "--bare", project.repositoryUrl, mirror]).catch(() => {
862
+ throw new Error("CODING_REPOSITORY_ACCESS_FAILED");
863
+ });
864
+ }
865
+ else {
866
+ const [{ stdout: bare }, { stdout: origin }] = await Promise.all([
867
+ execFileAsync("git", ["--git-dir", mirror, "rev-parse", "--is-bare-repository"]),
868
+ execFileAsync("git", ["--git-dir", mirror, "config", "--get", "remote.origin.url"]),
869
+ ]);
870
+ if (bare.trim() !== "true")
871
+ throw new Error("CODING_REPOSITORY_CACHE_NOT_BARE");
872
+ if ((await canonicalRepository(origin.trim())) !==
873
+ (await canonicalRepository(project.repositoryUrl)))
874
+ throw new Error("CODING_REPOSITORY_ORIGIN_MISMATCH");
875
+ }
876
+ if (!resume) {
877
+ // Shared worktrees own refs/heads/aip/*; fetching must only update remote-tracking refs.
878
+ await execFileAsync("git", [
879
+ "--git-dir",
880
+ mirror,
881
+ "config",
882
+ "--replace-all",
883
+ "remote.origin.fetch",
884
+ "+refs/heads/*:refs/remotes/origin/*",
885
+ ]);
886
+ await execFileAsync("git", ["--git-dir", mirror, "fetch", "origin"]).catch(() => {
887
+ throw new Error("CODING_REPOSITORY_ACCESS_FAILED");
888
+ });
889
+ await execFileAsync("git", [
890
+ "--git-dir",
891
+ mirror,
892
+ "cat-file",
893
+ "-e",
894
+ `refs/remotes/origin/${project.baselineBranch}`,
895
+ ]).catch(() => {
896
+ throw new Error("CODING_BASELINE_BRANCH_UNAVAILABLE");
897
+ });
898
+ }
899
+ const target = join(runRoot, safeName(project.name));
900
+ const expectedBranch = project.devBranch || `aip/${runId}/${project.id.slice(0, 8)}`;
901
+ const existingWorktree = await exists(target);
902
+ if (!existingWorktree) {
903
+ if (resume)
904
+ throw new Error("CODING_RUN_WORKTREE_MISSING");
905
+ await execFileAsync("git", [
906
+ "--git-dir",
907
+ mirror,
908
+ "worktree",
909
+ "add",
910
+ "-b",
911
+ expectedBranch,
912
+ target,
913
+ `refs/remotes/origin/${project.baselineBranch}`,
914
+ ]).catch(() => {
915
+ throw new Error("CODING_WORKTREE_CREATE_FAILED");
916
+ });
917
+ }
918
+ await validateWorktree(mirror, target, expectedBranch, resume || existingWorktree);
919
+ }
920
+ const onlyProject = projects.length === 1 ? projects[0] : null;
921
+ return onlyProject ? join(runRoot, safeName(onlyProject.name)) : runRoot;
922
+ }
923
+ async function validateWorktree(mirror, target, expectedBranch, resume) {
924
+ const facts = await Promise.all([
925
+ execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: target }),
926
+ execFileAsync("git", ["rev-parse", "--git-common-dir"], { cwd: target }),
927
+ execFileAsync("git", ["branch", "--show-current"], { cwd: target }),
928
+ ]).catch(() => {
929
+ throw new Error("CODING_WORKTREE_OWNERSHIP_INVALID");
930
+ });
931
+ const [{ stdout: inside }, { stdout: common }, { stdout: branch }] = facts;
932
+ if (inside.trim() !== "true")
933
+ throw new Error("CODING_WORKTREE_OWNERSHIP_INVALID");
934
+ const commonPath = common.trim();
935
+ const [actualCommon, expectedCommon] = await Promise.all([
936
+ realpath(commonPath.startsWith("/") ? commonPath : resolve(target, commonPath)),
937
+ realpath(mirror),
938
+ ]);
939
+ if (actualCommon !== expectedCommon)
940
+ throw new Error("CODING_WORKTREE_OWNERSHIP_INVALID");
941
+ if (branch.trim() !== expectedBranch)
942
+ throw new Error("CODING_WORKTREE_BRANCH_MISMATCH");
943
+ if (resume)
944
+ return;
945
+ const { stdout: status } = await execFileAsync("git", ["status", "--porcelain"], {
946
+ cwd: target,
947
+ });
948
+ if (status.trim())
949
+ throw new Error("CODING_WORKTREE_NOT_CLEAN");
950
+ }
951
+ async function canonicalRepository(value) {
952
+ if (value.startsWith("/"))
953
+ return realpath(value).catch(() => resolve(value));
954
+ if (value.startsWith("file://")) {
955
+ const path = new URL(value);
956
+ return realpath(path).catch(() => resolve(path.pathname));
957
+ }
958
+ return value.replace(/\/$/u, "");
959
+ }
960
+ export function initialPrompt(input, runRoot) {
961
+ const directory = deliveryDirectory(runRoot, input);
962
+ if (input.executionPurpose === "RELEASE")
963
+ 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。平台查询状态,不负责创建。仅说明或更新文档不退回研发;不执行实际发布、知识整理、关单。`;
964
+ const projects = input.projects
965
+ .map((p) => `- ${p.name}: ${join(runRoot, safeName(p.name))}(开发分支 ${p.devBranch} → 基准分支 ${p.baselineBranch};测试分支 ${p.testBranch})${p.id === input.primaryProjectId ? "(主项目)" : ""}`)
966
+ .join("\n");
967
+ return `${input.phase === "DEVELOPMENT" ? "当前是开发阶段:按员工与阶段配置完成工作,填写需求汇总 summary.md、plan.md、code_review.md、release_checklist.md 后调用 complete_development。由主管安排适合的成员协作。" : "当前是系统测试阶段:读取开发交接文件,随测试修复维护开发产出的 release_checklist.md,按需要安排成员协作。开始研发已授权将工单代码合并并 push 到项目已确认的 testBranch,触发测试环境自动部署;必须实际 push,只有本地合并不会触发部署。当前仓库使用共享 bare mirror,推送使用 git -c remote.origin.mirror=false push origin <已确认测试分支>,不要修改全局或共享缓存的 mirror 配置;操作后回到本工单分支。这是测试部署,不是生产上线;不得推送生产分支或执行生产发布。使用环境 Skill 观察部署目标和实际版本,将实际观察和验证结论写入测试报告;失败时修复、提交合并、等待部署、重测。人工打回复用当前环境。不默认重新审核,报告区分审核版本和后续修复。"} 需要业务判断时正常提问,使用 update_question 维护待回应问题;不要调用 report_blocker 表示普通提问。${input.inputConflict ? `正式输入已变化:先读取 ${directory}/prd.platform.md 和适用的 design.platform.md,对照合并到 prd.md/design.md;本地副本未被覆盖,对照文件不收集。` : ""}请完成工单 ${input.workItem.key}:${input.workItem.title}。项目:\n${projects}\n先用 read 读取 ${directory}/prd.md${input.technicalDesign ? ` 和 ${directory}/design.md` : ""}。\n在 ${directory} 完成 ${input.phase === "DEVELOPMENT" ? "summary.md、plan.md、code_review.md、release_checklist.md" : "summary.md、plan.md、report.md、code_review.md、release_checklist.md;test_plan.md 可选"}。summary.md 是本工单的问题、目标和范围汇总,不是实现摘要。release_checklist.md 是实际上线操作清单,覆盖项目与源/目标分支、SQL、Apollo、定时任务、Kafka/RabbitMQ资源、监控告警、发布顺序、验证及回滚;有设计以设计为预期并核对代码,没有设计从代码提取;未知环境事实标明待核实,不涉及写原因,不能用测试部署记录代替。全流程不以 commit ID 或 tree hash 判断是否需要补验。允许修改 PRD/Design,本轮将回收最终内容。报告包含各项目实际验证结果、版本、证据、未验证和遗留。只有上述固定槽位会被回收。\n${input.team ? "只向实际可用成员分派任务,无子成员时由主管自行完成并汇总。" : "自行完成当前阶段工作。"}团队自主选择开发、审核和测试方法,如实记录完成情况、未验证项及遗留问题,不编造结果。平台只检查必需产物,不要求指定成员、PASS组合或测试命令。由主管调用当前阶段的无参数完成工具,Runtime读取文件并上传。缺文件按工具错误补齐后再次提交。`;
968
+ }
969
+ async function uploadBatch(client, runId, batch) {
970
+ for (let attempt = 0;; attempt++) {
971
+ try {
972
+ await client.reportCodingTerminal(runId, batch);
973
+ return;
974
+ }
975
+ catch (error) {
976
+ if (error instanceof PlatformClientError && error.status < 500 && error.status !== 429)
977
+ throw error;
978
+ if (attempt >= 2)
979
+ throw new Error(`AIP_DELIVERY_UPLOAD_FAILED:同批材料尚未提交成功,可恢复重试;${error instanceof Error ? error.message : "网络错误"}`);
980
+ await new Promise((resolve) => setTimeout(resolve, 300 * (attempt + 1)));
981
+ }
982
+ }
983
+ }
984
+ function textFromMessage(content) {
985
+ if (typeof content === "string")
986
+ return content;
987
+ if (!Array.isArray(content))
988
+ return "";
989
+ return content
990
+ .filter((part) => Boolean(part &&
991
+ typeof part === "object" &&
992
+ part.type === "text" &&
993
+ typeof part.text === "string"))
994
+ .map((part) => part.text)
995
+ .join("\n");
996
+ }
997
+ function textFromResult(value) {
998
+ if (!value || typeof value !== "object")
999
+ return "";
1000
+ return textFromMessage(value.content).slice(0, 4000);
1001
+ }
1002
+ function toolResult(text) {
1003
+ return { content: [{ type: "text", text }], details: {} };
1004
+ }
1005
+ function safeName(value) {
1006
+ return basename(value).replace(/[^a-zA-Z0-9._-]/gu, "-") || "project";
1007
+ }
1008
+ async function exists(path) {
1009
+ try {
1010
+ await stat(path);
1011
+ return true;
1012
+ }
1013
+ catch {
1014
+ return false;
1015
+ }
1016
+ }
1017
+ async function collectProjectFacts(runRoot, projects, key, primaryProjectId) {
1018
+ return Promise.all(projects.map(async (project) => {
1019
+ const cwd = join(runRoot, safeName(project.name));
1020
+ const excludeKey = project.id === primaryProjectId ? key : undefined;
1021
+ const [{ stdout: head }, tree, { stdout: branch }] = await Promise.all([
1022
+ execFileAsync("git", ["rev-parse", "HEAD"], { cwd }),
1023
+ worktreeTreeHash(cwd, excludeKey),
1024
+ execFileAsync("git", ["branch", "--show-current"], { cwd }),
1025
+ ]);
1026
+ const commitSha = head.trim();
1027
+ return {
1028
+ projectId: project.id,
1029
+ projectName: project.name,
1030
+ commitSha,
1031
+ treeHash: tree,
1032
+ branch: branch.trim(),
1033
+ };
1034
+ }));
1035
+ }
1036
+ async function captureProjectRevisions(runRoot, projects, key, primaryProjectId) {
1037
+ return collectProjectFacts(runRoot, projects, key, primaryProjectId);
1038
+ }
1039
+ export async function worktreeTreeHash(cwd, key, committed = false, ref = "HEAD") {
1040
+ const temporary = await mkdtemp(join(tmpdir(), "aip-tested-tree-"));
1041
+ const index = join(temporary, "index");
1042
+ const options = { cwd, env: { ...process.env, GIT_INDEX_FILE: index } };
1043
+ try {
1044
+ await execFileAsync("git", ["read-tree", ref], options);
1045
+ if (!committed)
1046
+ await execFileAsync("git", ["add", "-A"], options);
1047
+ if (key) {
1048
+ const names = [
1049
+ "summary.md",
1050
+ "prd.md",
1051
+ "design.md",
1052
+ "plan.md",
1053
+ "test_plan.md",
1054
+ "report.md",
1055
+ "code_review.md",
1056
+ "release_checklist.md",
1057
+ "prd.platform.md",
1058
+ "design.platform.md",
1059
+ ];
1060
+ await execFileAsync("git", [
1061
+ "rm",
1062
+ "--cached",
1063
+ "--force", // This removes entries only from the dedicated temporary index.
1064
+ "--ignore-unmatch",
1065
+ "--",
1066
+ ...names.map((name) => `docs/aip/${key}/${name}`),
1067
+ ], options);
1068
+ }
1069
+ return (await execFileAsync("git", ["write-tree"], options)).stdout.trim();
1070
+ }
1071
+ finally {
1072
+ await rm(temporary, { recursive: true, force: true });
1073
+ }
1074
+ }
1075
+ export function redactHostText(value, roots = []) {
1076
+ let redacted = value;
1077
+ for (const root of [...roots].filter(Boolean).sort((a, b) => b.length - a.length))
1078
+ redacted = redacted.split(root).join("[本地路径]");
1079
+ redacted = redacted
1080
+ .replace(/data:image\/[^;,\s]+;base64,[A-Za-z0-9+/=]+/giu, "[图片内容已脱敏]")
1081
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/giu, "Bearer [已脱敏]")
1082
+ .replace(/\b(?:sk-[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,})\b/gu, "[凭证已脱敏]")
1083
+ .replace(/\b([A-Z0-9_]*(?:TOKEN|PASSWORD|SECRET|API_KEY|AUTH)[A-Z0-9_]*)=([^\s]+)/giu, "$1=[已脱敏]")
1084
+ .replace(/(["']?(?:access_token|refresh_token|id_token|api_key|password|secret|token)["']?\s*:\s*["']?)[^"'\s,}]+/giu, "$1[已脱敏]")
1085
+ .replace(/-----BEGIN [^-]+ PRIVATE KEY-----[\s\S]*?-----END [^-]+ PRIVATE KEY-----/gu, "[私钥已脱敏]")
1086
+ .replace(/(?:\/(?:Users|home|private\/var|var\/folders|tmp)\/[^\s'"`<>]*)/gu, "[本地路径]")
1087
+ .replace(/\b[A-Za-z]:\\(?:[^\s'"`<>]+\\?)+/gu, "[本地路径]");
1088
+ return redacted.slice(0, 100_000);
1089
+ }
1090
+ //# sourceMappingURL=coding-run-manager.js.map