@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.
- package/README.md +118 -1
- package/dist/agent-harness/index.d.ts +4 -0
- package/dist/agent-harness/index.d.ts.map +1 -0
- package/dist/agent-harness/index.js +4 -0
- package/dist/agent-harness/index.js.map +1 -0
- package/dist/agent-harness/pi-chat-model.d.ts +35 -0
- package/dist/agent-harness/pi-chat-model.d.ts.map +1 -0
- package/dist/agent-harness/pi-chat-model.js +285 -0
- package/dist/agent-harness/pi-chat-model.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +60 -23
- package/dist/cli.js.map +1 -1
- package/dist/coding-run-manager.d.ts +76 -16
- package/dist/coding-run-manager.d.ts.map +1 -1
- package/dist/coding-run-manager.js +550 -439
- package/dist/coding-run-manager.js.map +1 -1
- package/dist/coding-subagent-child.d.ts +1 -1
- package/dist/coding-subagent-child.d.ts.map +1 -1
- package/dist/coding-subagent-child.js +14 -21
- package/dist/coding-subagent-child.js.map +1 -1
- package/dist/coding-subagents.d.ts +2 -0
- package/dist/coding-subagents.d.ts.map +1 -1
- package/dist/coding-subagents.js +103 -119
- package/dist/coding-subagents.js.map +1 -1
- package/dist/daemon.d.ts +4 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +155 -43
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-files.d.ts +15 -12
- package/dist/delivery-files.d.ts.map +1 -1
- package/dist/delivery-files.js +124 -78
- package/dist/delivery-files.js.map +1 -1
- package/dist/delivery-tools.d.ts +28 -0
- package/dist/delivery-tools.d.ts.map +1 -0
- package/dist/delivery-tools.js +167 -0
- package/dist/delivery-tools.js.map +1 -0
- package/dist/diagnostics.d.ts +1 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +48 -14
- package/dist/diagnostics.js.map +1 -1
- package/dist/host-update.d.ts +3 -0
- package/dist/host-update.d.ts.map +1 -0
- package/dist/host-update.js +70 -0
- package/dist/host-update.js.map +1 -0
- package/dist/installer.d.ts +5 -8
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +155 -65
- package/dist/installer.js.map +1 -1
- package/dist/linux-service.d.ts +14 -0
- package/dist/linux-service.d.ts.map +1 -0
- package/dist/linux-service.js +63 -0
- package/dist/linux-service.js.map +1 -0
- package/dist/live-employee.d.ts +4 -3
- package/dist/live-employee.d.ts.map +1 -1
- package/dist/live-employee.js +222 -60
- package/dist/live-employee.js.map +1 -1
- package/dist/local-handoff-workspace.d.ts +20 -0
- package/dist/local-handoff-workspace.d.ts.map +1 -0
- package/dist/local-handoff-workspace.js +80 -0
- package/dist/local-handoff-workspace.js.map +1 -0
- package/dist/local-model-usage.d.ts +6 -0
- package/dist/local-model-usage.d.ts.map +1 -0
- package/dist/local-model-usage.js +40 -0
- package/dist/local-model-usage.js.map +1 -0
- package/dist/login-url.d.ts +3 -0
- package/dist/login-url.d.ts.map +1 -0
- package/dist/login-url.js +30 -0
- package/dist/login-url.js.map +1 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +27 -3
- package/dist/paths.js.map +1 -1
- package/dist/pi-model-service.d.ts +11 -0
- package/dist/pi-model-service.d.ts.map +1 -1
- package/dist/pi-model-service.js +28 -11
- package/dist/pi-model-service.js.map +1 -1
- package/dist/platform-client.d.ts +214 -6
- package/dist/platform-client.d.ts.map +1 -1
- package/dist/platform-client.js +66 -0
- package/dist/platform-client.js.map +1 -1
- package/dist/profile.d.ts +9 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +43 -0
- package/dist/profile.js.map +1 -0
- package/dist/project-facts.d.ts +19 -0
- package/dist/project-facts.d.ts.map +1 -0
- package/dist/project-facts.js +91 -0
- package/dist/project-facts.js.map +1 -0
- package/dist/provider-auth-error.d.ts +3 -0
- package/dist/provider-auth-error.d.ts.map +1 -0
- package/dist/provider-auth-error.js +12 -0
- package/dist/provider-auth-error.js.map +1 -0
- package/dist/qa-contract.d.ts +2 -0
- package/dist/qa-contract.d.ts.map +1 -0
- package/dist/qa-contract.js +165 -0
- package/dist/qa-contract.js.map +1 -0
- package/dist/qa-http-validation.js +145 -0
- package/dist/qa-project-checkout.d.ts +12 -0
- package/dist/qa-project-checkout.d.ts.map +1 -0
- package/dist/qa-project-checkout.js +68 -0
- package/dist/qa-project-checkout.js.map +1 -0
- package/dist/qa-session-manager.d.ts +101 -0
- package/dist/qa-session-manager.d.ts.map +1 -0
- package/dist/qa-session-manager.js +581 -0
- package/dist/qa-session-manager.js.map +1 -0
- package/dist/review-report.d.ts +3 -4
- package/dist/review-report.d.ts.map +1 -1
- package/dist/review-report.js +14 -12
- package/dist/review-report.js.map +1 -1
- package/dist/secure-json.d.ts.map +1 -1
- package/dist/secure-json.js +64 -2
- package/dist/secure-json.js.map +1 -1
- package/dist/stage-delivery.js +42 -25
- package/dist/state.d.ts +2 -2
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +6 -4
- package/dist/state.js.map +1 -1
- package/dist/test-cli.d.ts +2 -0
- package/dist/test-cli.d.ts.map +1 -0
- package/dist/test-cli.js +50 -0
- package/dist/test-cli.js.map +1 -0
- package/dist/test-executor.d.ts +44 -0
- package/dist/test-executor.d.ts.map +1 -0
- package/dist/test-executor.js +474 -0
- package/dist/test-executor.js.map +1 -0
- package/dist/test-reports.d.ts +13 -0
- package/dist/test-reports.d.ts.map +1 -0
- package/dist/test-reports.js +129 -0
- package/dist/test-reports.js.map +1 -0
- package/dist/test-run-manager.d.ts +24 -0
- package/dist/test-run-manager.d.ts.map +1 -0
- package/dist/test-run-manager.js +121 -0
- package/dist/test-run-manager.js.map +1 -0
- package/dist/tool-dispatch.d.ts +14 -0
- package/dist/tool-dispatch.d.ts.map +1 -0
- package/dist/tool-dispatch.js +56 -0
- package/dist/tool-dispatch.js.map +1 -0
- package/dist/windows-runtime.d.ts +9 -0
- package/dist/windows-runtime.d.ts.map +1 -0
- package/dist/windows-runtime.js +64 -0
- package/dist/windows-runtime.js.map +1 -0
- package/dist/windows-service.d.ts +20 -0
- package/dist/windows-service.d.ts.map +1 -0
- package/dist/windows-service.js +96 -0
- package/dist/windows-service.js.map +1 -0
- 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,
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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", {
|
|
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
|
-
|
|
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(
|
|
216
|
-
this.active.delete(
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
250
|
-
input.technicalDesign?.
|
|
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 =
|
|
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 = () =>
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
430
|
-
await
|
|
431
|
-
|
|
585
|
+
catch (error) {
|
|
586
|
+
await disposeRun();
|
|
587
|
+
throw error;
|
|
432
588
|
}
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
|
463
|
-
await delegation?.flush();
|
|
625
|
+
await emit.flush();
|
|
464
626
|
}
|
|
465
627
|
finally {
|
|
466
|
-
await
|
|
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:
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
let
|
|
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
|
|
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
|
-
|
|
770
|
-
|
|
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
|
|
841
|
-
|
|
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
|
-
|
|
1052
|
+
hostId: host.hostId,
|
|
1053
|
+
generation: host.generation,
|
|
852
1054
|
turnId,
|
|
853
1055
|
kind: "FAILED",
|
|
854
|
-
summary:
|
|
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
|
|
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
|
|
910
|
-
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
-
//
|
|
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
|
-
"
|
|
1124
|
-
"
|
|
1125
|
-
"
|
|
1126
|
-
expectedBranch
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
1190
|
-
const directory = deliveryDirectory(
|
|
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
|
-
|
|
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
|