@bf-meishijian/aip 0.0.11 → 0.0.17

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 (80) hide show
  1. package/README.md +25 -0
  2. package/dist/aip-submit-outcome/SKILL.md +47 -0
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +32 -6
  6. package/dist/cli.js.map +1 -1
  7. package/dist/coding-run-manager.d.ts +17 -1
  8. package/dist/coding-run-manager.d.ts.map +1 -1
  9. package/dist/coding-run-manager.js +186 -143
  10. package/dist/coding-run-manager.js.map +1 -1
  11. package/dist/coding-subagent-child.d.ts.map +1 -1
  12. package/dist/coding-subagent-child.js +13 -0
  13. package/dist/coding-subagent-child.js.map +1 -1
  14. package/dist/daemon.d.ts +1 -0
  15. package/dist/daemon.d.ts.map +1 -1
  16. package/dist/daemon.js +42 -3
  17. package/dist/daemon.js.map +1 -1
  18. package/dist/delivery-tools.d.ts.map +1 -1
  19. package/dist/delivery-tools.js +13 -0
  20. package/dist/delivery-tools.js.map +1 -1
  21. package/dist/diagnostic-log.d.ts +19 -0
  22. package/dist/diagnostic-log.d.ts.map +1 -0
  23. package/dist/diagnostic-log.js +59 -0
  24. package/dist/diagnostic-log.js.map +1 -0
  25. package/dist/diagnostics.d.ts +5 -1
  26. package/dist/diagnostics.d.ts.map +1 -1
  27. package/dist/diagnostics.js +20 -8
  28. package/dist/diagnostics.js.map +1 -1
  29. package/dist/git-process.d.ts +23 -0
  30. package/dist/git-process.d.ts.map +1 -0
  31. package/dist/git-process.js +162 -0
  32. package/dist/git-process.js.map +1 -0
  33. package/dist/git-transport.d.ts.map +1 -1
  34. package/dist/git-transport.js +1 -3
  35. package/dist/git-transport.js.map +1 -1
  36. package/dist/host-tools.d.ts +14 -0
  37. package/dist/host-tools.d.ts.map +1 -0
  38. package/dist/host-tools.js +131 -0
  39. package/dist/host-tools.js.map +1 -0
  40. package/dist/installer.d.ts.map +1 -1
  41. package/dist/installer.js +3 -0
  42. package/dist/installer.js.map +1 -1
  43. package/dist/live-employee.d.ts.map +1 -1
  44. package/dist/live-employee.js +18 -23
  45. package/dist/live-employee.js.map +1 -1
  46. package/dist/local-handoff-workspace.d.ts.map +1 -1
  47. package/dist/local-handoff-workspace.js +22 -8
  48. package/dist/local-handoff-workspace.js.map +1 -1
  49. package/dist/outcome-cli.d.ts +2 -0
  50. package/dist/outcome-cli.d.ts.map +1 -0
  51. package/dist/outcome-cli.js +83 -0
  52. package/dist/outcome-cli.js.map +1 -0
  53. package/dist/pi-model-service.d.ts +2 -1
  54. package/dist/pi-model-service.d.ts.map +1 -1
  55. package/dist/pi-model-service.js +34 -34
  56. package/dist/pi-model-service.js.map +1 -1
  57. package/dist/platform-client.d.ts +7 -2
  58. package/dist/platform-client.d.ts.map +1 -1
  59. package/dist/platform-client.js +7 -3
  60. package/dist/platform-client.js.map +1 -1
  61. package/dist/run-resources.d.ts +18 -0
  62. package/dist/run-resources.d.ts.map +1 -0
  63. package/dist/run-resources.js +115 -0
  64. package/dist/run-resources.js.map +1 -0
  65. package/dist/runtime-environment.d.ts +2 -0
  66. package/dist/runtime-environment.d.ts.map +1 -0
  67. package/dist/runtime-environment.js +7 -0
  68. package/dist/runtime-environment.js.map +1 -0
  69. package/dist/runtime-search-tools.d.ts +3 -0
  70. package/dist/runtime-search-tools.d.ts.map +1 -0
  71. package/dist/runtime-search-tools.js +30 -0
  72. package/dist/runtime-search-tools.js.map +1 -0
  73. package/dist/secure-json.d.ts +8 -0
  74. package/dist/secure-json.d.ts.map +1 -1
  75. package/dist/secure-json.js +33 -3
  76. package/dist/secure-json.js.map +1 -1
  77. package/dist/state.d.ts +2 -2
  78. package/dist/state.js +3 -3
  79. package/dist/state.js.map +1 -1
  80. package/package.json +14 -23
@@ -1,10 +1,9 @@
1
- import { execFile } from "node:child_process";
2
1
  import { createHash } from "node:crypto";
3
2
  import { mkdir, open, realpath, stat } from "node:fs/promises";
4
3
  import { basename, dirname, join, resolve } from "node:path";
5
- import { promisify } from "node:util";
6
4
  import { createAgentSession, DefaultResourceLoader, SessionManager, } from "@earendil-works/pi-coding-agent";
7
5
  import { deliveryTools } from "./delivery-tools.js";
6
+ import { execGitFile as execFileAsync, gitFailure, missingGitRef } from "./git-process.js";
8
7
  import { ensureSshOrigin, gitTransportUrl, matchesRepository, SSH_ACCESS_HINT, } from "./git-transport.js";
9
8
  import { logHostEvent } from "./host-log.js";
10
9
  import { inspectHandoffWorkspace, synchronizeLocalIntake, synchronizeRemoteWorkspace, } from "./local-handoff-workspace.js";
@@ -18,10 +17,9 @@ import { liveEmployee } from "./live-employee.js";
18
17
  import { paths } from "./paths.js";
19
18
  import { PiMessageQueue } from "./pi-message-queue.js";
20
19
  import { PlatformClientError } from "./platform-client.js";
21
- import { materializeReviewReport } from "./review-report.js";
20
+ import { inspectRunResources } from "./run-resources.js";
22
21
  import { readJson, writeSecureJson } from "./secure-json.js";
23
22
  import { stageEmployeeIds } from "./stage-delivery.js";
24
- const execFileAsync = promisify(execFile);
25
23
  const EVENT_APPEND_MAX_ATTEMPTS = 8;
26
24
  export class CodingRunManager {
27
25
  pi;
@@ -40,6 +38,12 @@ export class CodingRunManager {
40
38
  }));
41
39
  if (!command.run)
42
40
  throw new Error("CODING_RUN_COMMAND_INVALID");
41
+ if (command.run.action === "CHECK_RESOURCES") {
42
+ const input = command.run.runInput;
43
+ return {
44
+ resources: await inspectRunResources(command.run.runId, input, this.active.has(command.run.runId) || this.active.has(`${command.run.runId}:discussion`)),
45
+ };
46
+ }
43
47
  if (command.run.action === "STOP" && command.run.runInput?.discussionTurn) {
44
48
  const discussion = this.active.get(`${command.run.runId}:discussion`);
45
49
  if (discussion && discussion.turnId === command.run.turnId) {
@@ -60,31 +64,6 @@ export class CodingRunManager {
60
64
  }
61
65
  return;
62
66
  }
63
- if (command.run.action === "REVIEW_REPORT") {
64
- const report = command.run.reviewReport;
65
- if (!report || !command.run.runInput)
66
- throw new Error("REVIEW_REPORT_INPUT_MISSING");
67
- const input = command.run.runInput;
68
- const runRoot = join(paths().runWorktrees, command.run.runId);
69
- if (input.transferContinuation) {
70
- const notes = await inspectPriorTransferWorkspaces(paths().runWorktrees, input.transferWorkspaces?.priorRunIds ?? [], input.projects);
71
- if (notes.length) {
72
- const state = await readJson(join(paths().runState, `${command.run.runId}.json`));
73
- await eventEmitter(client, host, command.run.runId, command.run.turnId, state?.lastEventSequence ?? 0)("MESSAGE_COMPLETED", { contentMarkdown: `旧工作区检查:\n${notes.join("\n")}` });
74
- }
75
- await mkdir(runRoot, { recursive: true, mode: 0o700 });
76
- await prepareProjects(paths().repositoryCache, runRoot, input.workItem.key, input.projects, false, true, input.transferWorkspaces?.uninitializedProjectIds);
77
- }
78
- const files = await materializeReviewReport(runRoot, input, report.files, readMaterial);
79
- await client.acknowledgeReviewReport(command.run.runId, {
80
- hostId: host.hostId,
81
- generation: host.generation,
82
- turnId: command.run.turnId,
83
- roundId: report.roundId,
84
- files,
85
- });
86
- return;
87
- }
88
67
  if (command.run.action === "RECALL_QUEUE") {
89
68
  return {
90
69
  recalledMessageIds: this.active.get(command.run.runId)?.observed.messageQueue?.recall() ?? [],
@@ -130,7 +109,9 @@ export class CodingRunManager {
130
109
  const active = this.active.get(command.run.runId);
131
110
  if (active) {
132
111
  await active.finished;
133
- if (active.stopping || active.observed.userIntervened)
112
+ if (active.stopping ||
113
+ active.observed.userIntervened ||
114
+ active.observed.obsoleteContinuationAudits?.has(command.run.continuation?.auditId ?? ""))
134
115
  return;
135
116
  }
136
117
  if (!command.run.runInput || !command.run.contentMarkdown || !command.run.continuation)
@@ -306,14 +287,32 @@ export class CodingRunManager {
306
287
  await mkdir(runRoot, { recursive: true, mode: 0o700 });
307
288
  const stateFile = join(local.runState, `${runId}.json`);
308
289
  const storedState = await readJson(stateFile);
309
- if (storedState?.phaseSessions &&
310
- Object.keys(storedState.phaseSessions).length &&
311
- storedState.harness !== "pi")
312
- throw new Error("PRIOR_HARNESS_SESSION_UNSUPPORTED:请新建执行,历史文件和工作目录已保留。");
313
290
  for (const project of input.projects)
314
291
  project.devBranch ||=
315
292
  storedState?.mrContext?.projects.find((p) => p.id === project.id)?.devBranch ||
316
293
  `aip/${input.workItem.key}`;
294
+ let recoveryWorkspacePresent = false;
295
+ if (input.sessionRecovery) {
296
+ const facts = await inspectRunResources(runId, {
297
+ ...input,
298
+ expectedSessionId: input.sessionRecovery.expectedSessionId,
299
+ });
300
+ recoveryWorkspacePresent = facts.workspace === "PRESENT";
301
+ if (facts.workspace === "INVALID")
302
+ throw new Error("CODING_RUN_WORKTREE_INVALID");
303
+ if (input.sessionRecovery.mode === "RESUME" && facts.workspace !== "PRESENT")
304
+ throw new Error("CODING_RUN_WORKTREE_MISSING");
305
+ if (input.sessionRecovery.mode === "RESUME" && facts.session !== "PRESENT")
306
+ throw new Error(facts.session === "INVALID" ? "PI_SESSION_INVALID" : "PI_SESSION_NOT_FOUND");
307
+ if (input.sessionRecovery.mode === "NEW_SESSION" &&
308
+ facts.workspace !== "PRESENT" &&
309
+ facts.remoteBranch !== "PRESENT")
310
+ throw new Error("CODING_BRANCH_NOT_PUBLISHED");
311
+ }
312
+ if (storedState?.phaseSessions &&
313
+ Object.keys(storedState.phaseSessions).length &&
314
+ storedState.harness !== "pi")
315
+ throw new Error("PRIOR_HARNESS_SESSION_UNSUPPORTED:请新建执行,历史文件和工作目录已保留。");
317
316
  const transferNotes = input.transferContinuation && !storedState?.prepared
318
317
  ? await inspectPriorTransferWorkspaces(local.runWorktrees, input.transferWorkspaces?.priorRunIds ?? [], input.projects)
319
318
  : [];
@@ -324,7 +323,7 @@ export class CodingRunManager {
324
323
  : runRoot
325
324
  : deploymentSession
326
325
  ? await prepareDeploymentProjects(runRoot, input.projects, input.deploymentConfiguration)
327
- : await prepareProjects(local.repositoryCache, runRoot, input.workItem.key, input.projects, Boolean(storedState?.prepared), input.transferContinuation === true, input.transferWorkspaces?.uninitializedProjectIds);
326
+ : await prepareProjects(local.repositoryCache, runRoot, input.workItem.key, input.projects, input.sessionRecovery ? recoveryWorkspacePresent : Boolean(storedState?.prepared), input.transferContinuation === true || input.sessionRecovery?.mode === "NEW_SESSION", input.transferWorkspaces?.uninitializedProjectIds, Boolean(input.sessionRecovery));
328
327
  let appliedLocalIntake = storedState?.appliedLocalIntake;
329
328
  if (!input.discussionTurn && input.localIntake && appliedLocalIntake !== input.localIntake.id) {
330
329
  for (const project of input.projects) {
@@ -375,10 +374,16 @@ export class CodingRunManager {
375
374
  generation: host.generation,
376
375
  }
377
376
  : undefined;
378
- const phaseSessions = storedState?.phaseSessions ?? {};
377
+ const phaseSessions = { ...storedState?.phaseSessions };
379
378
  const sessionPhase = input.executionPurpose === "RELEASE" ? "DEVELOPMENT" : input.phase;
380
- let sessionRef = newDeploymentRequest ? undefined : phaseSessions[sessionPhase];
381
- let lastEventSequence = storedState?.lastEventSequence ?? 0;
379
+ let sessionRef = input.sessionRecovery
380
+ ? input.sessionRecovery.mode === "NEW_SESSION"
381
+ ? undefined
382
+ : (input.sessionRecovery.expectedSessionId ?? phaseSessions[sessionPhase])
383
+ : newDeploymentRequest
384
+ ? undefined
385
+ : phaseSessions[sessionPhase];
386
+ let lastEventSequence = Math.max(storedState?.lastEventSequence ?? 0, input.sessionRecovery?.lastEventSequence ?? 0);
382
387
  const observed = {
383
388
  userIntervened: input.autoContinueEnabled === false,
384
389
  activeTasks: new Map(),
@@ -585,7 +590,7 @@ export class CodingRunManager {
585
590
  conversationFingerprint = await workspaceFingerprint(runRoot, input);
586
591
  prompt = continuation
587
592
  ? resumePrompt
588
- : `${initialPrompt(input, runRoot, true, developmentRoot)}${transferNotes.length ? `\n\n旧工作区检查:\n${transferNotes.join("\n")}` : ""}${resumePrompt ? `\n\n用户本次消息:${resumePrompt}` : resume && !resumePrompt ? "\n\n继续当前会话未完成的工作,先读取最新文件和交接结果。" : ""}`;
593
+ : `${initialPrompt(input, runRoot, true, developmentRoot)}${input.sessionRecovery?.context ? `\n\n平台恢复上下文(历史事实,不是用户新消息):\n${input.sessionRecovery.context}` : ""}${transferNotes.length ? `\n\n旧工作区检查:\n${transferNotes.join("\n")}` : ""}${resumePrompt ? `\n\n用户本次消息:${resumePrompt}` : resume && !resumePrompt ? "\n\n继续当前会话未完成的工作,先读取最新文件和交接结果。" : ""}`;
589
594
  }
590
595
  catch (error) {
591
596
  await disposeRun();
@@ -753,7 +758,7 @@ export async function openPiSession(options) {
753
758
  const listed = await SessionManager.list(options.cwd, options.sessionDir);
754
759
  const match = listed.find((item) => item.id === options.existingId);
755
760
  if (!match)
756
- throw new Error("PI_SESSION_NOT_FOUND:请新建执行,历史文件已保留。");
761
+ throw new Error("PI_SESSION_NOT_FOUND");
757
762
  sessionManager = SessionManager.open(match.path, options.sessionDir, options.cwd);
758
763
  }
759
764
  else {
@@ -862,6 +867,8 @@ function subscribeSession(session, emit, observed, redact) {
862
867
  };
863
868
  session.subscribe((event) => {
864
869
  if (event.type === "agent_start") {
870
+ observed.executionRevision = (observed.executionRevision ?? 0) + 1;
871
+ observed.awaitingDecision = false;
865
872
  publishContext();
866
873
  if (observed.messageQueue)
867
874
  void emit("MESSAGE_QUEUE_UPDATED", { mainAgentRunning: true });
@@ -1018,100 +1025,126 @@ async function driveToTerminal(session, initial, observed, client, host, runId,
1018
1025
  const result = await outcome;
1019
1026
  if (!result.ok)
1020
1027
  throw result.error;
1021
- await session.waitForIdle();
1022
- observed.awaitingDecision = true;
1023
- if (observed.stopping)
1024
- return;
1025
- const stopReason = observed.terminalClosed
1026
- ? "TERMINAL_CLOSED"
1027
- : (observed.lastModelStopReason ?? "idle");
1028
- const endFact = { phase, stopReason, nativeStopReason: observed.lastModelStopReason ?? null };
1029
- if (observed.terminalReached) {
1030
- await emit("EXECUTION_DECIDED", {
1031
- ...endFact,
1032
- decision: "SKIP",
1033
- reason: observed.deploymentCheckPaused ? "DEPLOYMENT_CHECK_PAUSED" : "TERMINAL_ACCEPTED",
1034
- model: null,
1035
- });
1036
- return;
1037
- }
1038
- if (observed.lastModelStopReason === "aborted" ||
1039
- (observed.lastModelStopReason === "error" && observed.nativeWillRetry !== true)) {
1040
- const loginRequired = isProviderLoginRequired(observed.lastModelError);
1041
- const code = loginRequired
1042
- ? "PROVIDER_LOGIN_REQUIRED"
1043
- : observed.lastModelError?.split("\n")[0]?.slice(0, 200) ||
1044
- (observed.lastModelStopReason === "aborted" ? "PI_MODEL_ABORTED" : "MODEL_REQUEST_FAILED");
1045
- const message = loginRequired
1046
- ? "模型服务登录已失效,请到本地开发环境重新登录对应订阅后重试。"
1047
- : (observed.lastModelError ?? code);
1048
- await emit("EXECUTION_DECIDED", {
1049
- ...endFact,
1050
- decision: "SKIP",
1051
- reason: "MODEL_ERROR",
1052
- model: null,
1053
- });
1028
+ while (!observed.stopping) {
1029
+ await session.waitForIdle();
1030
+ if (session.isIdle === false)
1031
+ continue;
1032
+ const executionRevision = observed.executionRevision ?? 0;
1033
+ const superseded = () => (observed.executionRevision ?? 0) !== executionRevision;
1034
+ observed.awaitingDecision = true;
1054
1035
  if (observed.stopping)
1055
1036
  return;
1056
- await client.reportCodingTerminal(runId, {
1057
- hostId: host.hostId,
1058
- generation: host.generation,
1059
- turnId,
1060
- kind: "FAILED",
1061
- summary: message,
1062
- knownIssues: "",
1063
- failureCode: code,
1064
- projectFacts: [],
1065
- testFacts: [],
1066
- });
1067
- observed.terminalReached = true;
1068
- await emit("RUN_FAILED", { phase, code, message });
1069
- return;
1070
- }
1071
- // Judge reads durable messages. It queues automatic work through the same command
1072
- // claim boundary that gives user messages and stop priority.
1073
- await emit.flush();
1074
- if (observed.stopping)
1075
- return;
1076
- let decision;
1077
- try {
1078
- decision = await client.evaluateContinuation(runId, {
1079
- hostId: host.hostId,
1080
- generation: host.generation,
1081
- turnId,
1082
- stopReason,
1083
- ...(observed.lastDeliveryError
1084
- ? { lastDeliveryError: observed.lastDeliveryError.slice(0, 2000) }
1085
- : {}),
1086
- });
1087
- }
1088
- catch (error) {
1037
+ const stopReason = observed.terminalClosed
1038
+ ? "TERMINAL_CLOSED"
1039
+ : (observed.lastModelStopReason ?? "idle");
1040
+ const endFact = { phase, stopReason, nativeStopReason: observed.lastModelStopReason ?? null };
1041
+ if (observed.terminalReached) {
1042
+ await emit("EXECUTION_DECIDED", {
1043
+ ...endFact,
1044
+ decision: "SKIP",
1045
+ reason: observed.deploymentCheckPaused ? "DEPLOYMENT_CHECK_PAUSED" : "TERMINAL_ACCEPTED",
1046
+ model: null,
1047
+ });
1048
+ return;
1049
+ }
1050
+ if (observed.lastModelStopReason === "aborted" ||
1051
+ (observed.lastModelStopReason === "error" && observed.nativeWillRetry !== true)) {
1052
+ const loginRequired = isProviderLoginRequired(observed.lastModelError);
1053
+ const code = loginRequired
1054
+ ? "PROVIDER_LOGIN_REQUIRED"
1055
+ : observed.lastModelError?.split("\n")[0]?.slice(0, 200) ||
1056
+ (observed.lastModelStopReason === "aborted"
1057
+ ? "PI_MODEL_ABORTED"
1058
+ : "MODEL_REQUEST_FAILED");
1059
+ const message = loginRequired
1060
+ ? "模型服务登录已失效,请到本地开发环境重新登录对应订阅后重试。"
1061
+ : (observed.lastModelError ?? code);
1062
+ await emit("EXECUTION_DECIDED", {
1063
+ ...endFact,
1064
+ decision: "SKIP",
1065
+ reason: "MODEL_ERROR",
1066
+ model: null,
1067
+ });
1068
+ if (observed.stopping)
1069
+ return;
1070
+ await client.reportCodingTerminal(runId, {
1071
+ hostId: host.hostId,
1072
+ generation: host.generation,
1073
+ turnId,
1074
+ kind: "FAILED",
1075
+ summary: message,
1076
+ knownIssues: "",
1077
+ failureCode: code,
1078
+ projectFacts: [],
1079
+ testFacts: [],
1080
+ });
1081
+ observed.terminalReached = true;
1082
+ await emit("RUN_FAILED", { phase, code, message });
1083
+ return;
1084
+ }
1085
+ // Judge reads durable messages. It queues automatic work through the same command
1086
+ // claim boundary that gives user messages and stop priority.
1087
+ await emit.flush();
1088
+ if (observed.stopping)
1089
+ return;
1090
+ if (superseded())
1091
+ continue;
1092
+ let decision;
1093
+ try {
1094
+ decision = await client.evaluateContinuation(runId, {
1095
+ hostId: host.hostId,
1096
+ generation: host.generation,
1097
+ turnId,
1098
+ stopReason,
1099
+ ...(observed.lastDeliveryError
1100
+ ? { lastDeliveryError: observed.lastDeliveryError.slice(0, 2000) }
1101
+ : {}),
1102
+ });
1103
+ }
1104
+ catch (error) {
1105
+ if (observed.stopping)
1106
+ return;
1107
+ if (superseded())
1108
+ continue;
1109
+ await emit("EXECUTION_DECIDED", {
1110
+ ...endFact,
1111
+ decision: "SKIP",
1112
+ reason: "JUDGE_REQUEST_FAILED",
1113
+ model: null,
1114
+ explanation: error instanceof Error ? error.message : String(error),
1115
+ });
1116
+ if (observed.handingOffInput)
1117
+ return;
1118
+ if (superseded())
1119
+ continue;
1120
+ await completeConversation();
1121
+ observed.terminalReached = true;
1122
+ return;
1123
+ }
1089
1124
  if (observed.stopping)
1090
1125
  return;
1091
1126
  await emit("EXECUTION_DECIDED", {
1092
1127
  ...endFact,
1093
- decision: "SKIP",
1094
- reason: "JUDGE_REQUEST_FAILED",
1095
- model: null,
1096
- explanation: error instanceof Error ? error.message : String(error),
1128
+ ...decision,
1129
+ ...(superseded() ? { decision: "SKIP", reason: "EXECUTION_RESUMED" } : {}),
1097
1130
  });
1098
- if (observed.handingOffInput)
1131
+ if (superseded()) {
1132
+ if (decision.auditId) {
1133
+ observed.obsoleteContinuationAudits ??= new Set();
1134
+ observed.obsoleteContinuationAudits.add(decision.auditId);
1135
+ }
1136
+ continue;
1137
+ }
1138
+ // The platform has queued the continuation; never execute the returned prompt
1139
+ // directly, because a user can cancel it before the host claims the command.
1140
+ if (decision.decision === "CONTINUE")
1141
+ return;
1142
+ if (observed.stopping || observed.handingOffInput)
1099
1143
  return;
1100
1144
  await completeConversation();
1101
1145
  observed.terminalReached = true;
1102
1146
  return;
1103
1147
  }
1104
- if (observed.stopping)
1105
- return;
1106
- await emit("EXECUTION_DECIDED", { ...endFact, ...decision });
1107
- // The platform has queued the continuation; never execute the returned prompt
1108
- // directly, because a user can cancel it before the host claims the command.
1109
- if (decision.decision === "CONTINUE")
1110
- return;
1111
- if (observed.stopping || observed.handingOffInput)
1112
- return;
1113
- await completeConversation();
1114
- observed.terminalReached = true;
1115
1148
  }
1116
1149
  export async function prepareDeploymentProjects(runRoot, projects, configuration) {
1117
1150
  await mkdir(runRoot, { recursive: true, mode: 0o700 });
@@ -1129,8 +1162,8 @@ export async function prepareDeploymentProjects(runRoot, projects, configuration
1129
1162
  "--",
1130
1163
  gitTransportUrl(project.repositoryUrl, project.sshRepositoryUrl),
1131
1164
  target,
1132
- ]).catch(() => {
1133
- throw new Error(`DEPLOYMENT_REPOSITORY_PREPARE_FAILED:${SSH_ACCESS_HINT}`);
1165
+ ]).catch((error) => {
1166
+ throw gitFailure(`DEPLOYMENT_REPOSITORY_PREPARE_FAILED:${SSH_ACCESS_HINT}`, error);
1134
1167
  });
1135
1168
  }
1136
1169
  // Reuse the interrupted deployment as-is; the employee resolves its Git state.
@@ -1147,7 +1180,7 @@ export async function prepareDeploymentProjects(runRoot, projects, configuration
1147
1180
  return projects.length === 1 ? join(runRoot, safeName(projects[0].name)) : runRoot;
1148
1181
  }
1149
1182
  const repositoryPreparations = new Map();
1150
- export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects, resume, verifyRemote = false, uninitializedProjectIds = []) {
1183
+ export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects, resume, verifyRemote = false, uninitializedProjectIds = [], preserveExisting = false) {
1151
1184
  await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
1152
1185
  for (const project of projects) {
1153
1186
  const mirror = join(cacheRoot, `${project.id}.git`);
@@ -1162,14 +1195,23 @@ export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects,
1162
1195
  await previous;
1163
1196
  try {
1164
1197
  logHostEvent("repository_preparation_started", { projectId: project.id, workItemKey });
1198
+ const target = join(runRoot, safeName(project.name));
1199
+ const expectedBranch = project.devBranch || `aip/${workItemKey}`;
1200
+ const existingWorktree = await exists(target);
1201
+ // 会话接续只校验并复用现有现场;远端同步属于交接流程,不应覆盖或阻塞本机成果。
1202
+ if (preserveExisting && existingWorktree) {
1203
+ await validateWorktree(mirror, target, expectedBranch, true, project.repositoryUrl, project.sshRepositoryUrl);
1204
+ logHostEvent("repository_preparation_completed", { projectId: project.id, workItemKey });
1205
+ continue;
1206
+ }
1165
1207
  if (!(await exists(mirror))) {
1166
1208
  await execFileAsync("git", [
1167
1209
  "clone",
1168
1210
  "--bare",
1169
1211
  gitTransportUrl(project.repositoryUrl, project.sshRepositoryUrl),
1170
1212
  mirror,
1171
- ]).catch(() => {
1172
- throw new Error(`CODING_REPOSITORY_ACCESS_FAILED:${SSH_ACCESS_HINT}`);
1213
+ ]).catch((error) => {
1214
+ throw gitFailure(`CODING_REPOSITORY_ACCESS_FAILED:${SSH_ACCESS_HINT}`, error);
1173
1215
  });
1174
1216
  }
1175
1217
  else {
@@ -1193,13 +1235,10 @@ export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects,
1193
1235
  "remote.origin.fetch",
1194
1236
  "+refs/heads/*:refs/remotes/origin/*",
1195
1237
  ]);
1196
- await execFileAsync("git", ["--git-dir", mirror, "fetch", "--prune", "origin"]).catch(() => {
1197
- throw new Error(`CODING_REPOSITORY_ACCESS_FAILED:${SSH_ACCESS_HINT}`);
1238
+ await execFileAsync("git", ["--git-dir", mirror, "fetch", "--prune", "origin"]).catch((error) => {
1239
+ throw gitFailure(`CODING_REPOSITORY_ACCESS_FAILED:${SSH_ACCESS_HINT}`, error);
1198
1240
  });
1199
1241
  }
1200
- const target = join(runRoot, safeName(project.name));
1201
- const expectedBranch = project.devBranch || `aip/${workItemKey}`;
1202
- const existingWorktree = await exists(target);
1203
1242
  if (!existingWorktree) {
1204
1243
  if (resume)
1205
1244
  throw new Error("CODING_RUN_WORKTREE_MISSING");
@@ -1210,7 +1249,7 @@ export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects,
1210
1249
  "--verify",
1211
1250
  "--quiet",
1212
1251
  `refs/remotes/origin/${expectedBranch}`,
1213
- ]).then(() => true, () => false);
1252
+ ]).then(() => true, missingGitRef);
1214
1253
  const localExists = await execFileAsync("git", [
1215
1254
  "--git-dir",
1216
1255
  mirror,
@@ -1218,7 +1257,7 @@ export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects,
1218
1257
  "--verify",
1219
1258
  "--quiet",
1220
1259
  `refs/heads/${expectedBranch}`,
1221
- ]).then(() => true, () => false);
1260
+ ]).then(() => true, missingGitRef);
1222
1261
  if (remoteExists) {
1223
1262
  // A new execution owns its branch ref; an old execution keeps its worktree untouched.
1224
1263
  await execFileAsync("git", [
@@ -1228,8 +1267,8 @@ export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects,
1228
1267
  expectedBranch,
1229
1268
  gitTransportUrl(project.repositoryUrl, project.sshRepositoryUrl),
1230
1269
  target,
1231
- ]).catch(() => {
1232
- throw new Error(`CODING_REPOSITORY_ACCESS_FAILED:${SSH_ACCESS_HINT}`);
1270
+ ]).catch((error) => {
1271
+ throw gitFailure(`CODING_REPOSITORY_ACCESS_FAILED:${SSH_ACCESS_HINT}`, error);
1233
1272
  });
1234
1273
  }
1235
1274
  else {
@@ -1243,8 +1282,8 @@ export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects,
1243
1282
  "cat-file",
1244
1283
  "-e",
1245
1284
  `refs/remotes/origin/${project.baselineBranch}`,
1246
- ]).catch(() => {
1247
- throw new Error("CODING_BASELINE_BRANCH_UNAVAILABLE");
1285
+ ]).catch((error) => {
1286
+ throw gitFailure("CODING_BASELINE_BRANCH_UNAVAILABLE", error);
1248
1287
  });
1249
1288
  await execFileAsync("git", [
1250
1289
  "--git-dir",
@@ -1255,8 +1294,8 @@ export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects,
1255
1294
  expectedBranch,
1256
1295
  target,
1257
1296
  `refs/remotes/origin/${project.baselineBranch}`,
1258
- ]).catch(() => {
1259
- throw new Error("CODING_WORKTREE_CREATE_FAILED");
1297
+ ]).catch((error) => {
1298
+ throw gitFailure("CODING_WORKTREE_CREATE_FAILED", error);
1260
1299
  });
1261
1300
  }
1262
1301
  }
@@ -1279,8 +1318,8 @@ async function validateWorktree(mirror, target, expectedBranch, resume, reposito
1279
1318
  execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: target }),
1280
1319
  execFileAsync("git", ["rev-parse", "--git-common-dir"], { cwd: target }),
1281
1320
  execFileAsync("git", ["branch", "--show-current"], { cwd: target }),
1282
- ]).catch(() => {
1283
- throw new Error("CODING_WORKTREE_OWNERSHIP_INVALID");
1321
+ ]).catch((error) => {
1322
+ throw gitFailure("CODING_WORKTREE_OWNERSHIP_INVALID", error);
1284
1323
  });
1285
1324
  const [{ stdout: inside }, { stdout: common }, { stdout: branch }] = facts;
1286
1325
  if (inside.trim() !== "true")
@@ -1288,7 +1327,11 @@ async function validateWorktree(mirror, target, expectedBranch, resume, reposito
1288
1327
  const commonPath = common.trim();
1289
1328
  const [actualCommon, expectedCommon] = await Promise.all([
1290
1329
  realpath(commonPath.startsWith("/") ? commonPath : resolve(target, commonPath)),
1291
- realpath(mirror),
1330
+ realpath(mirror).catch((error) => {
1331
+ if (error.code === "ENOENT")
1332
+ return null;
1333
+ throw error;
1334
+ }),
1292
1335
  ]);
1293
1336
  if (actualCommon !== expectedCommon) {
1294
1337
  if (actualCommon !== (await realpath(join(target, ".git"))))